From 1cf31229d81f59fadd96cb9c3cf17bb00268ce79 Mon Sep 17 00:00:00 2001 From: Pika Date: Mon, 6 Apr 2020 13:49:35 -0400 Subject: [PATCH 001/724] Use embedded title for other track types --- MediaBrowser.Model/Entities/MediaStream.cs | 197 +++++++++++---------- 1 file changed, 106 insertions(+), 91 deletions(-) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index e7e8d7cecd..68e0242a97 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -77,106 +77,121 @@ namespace MediaBrowser.Model.Entities { get { - if (Type == MediaStreamType.Audio) + switch (Type) { - //if (!string.IsNullOrEmpty(Title)) - //{ - // return AddLanguageIfNeeded(Title); - //} + case MediaStreamType.Audio: + { + var attributes = new List(); - var attributes = new List(); + if (!string.IsNullOrEmpty(Language)) + { + attributes.Add(StringHelper.FirstToUpper(Language)); + } - if (!string.IsNullOrEmpty(Language)) - { - attributes.Add(StringHelper.FirstToUpper(Language)); - } - if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) - { - attributes.Add(AudioCodec.GetFriendlyName(Codec)); - } - else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) - { - attributes.Add(Profile); + if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) + { + attributes.Add(AudioCodec.GetFriendlyName(Codec)); + } + else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) + { + attributes.Add(Profile); + } + + if (!string.IsNullOrEmpty(ChannelLayout)) + { + attributes.Add(ChannelLayout); + } + else if (Channels.HasValue) + { + attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch"); + } + + if (IsDefault) + { + attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + } + + if (!string.IsNullOrEmpty(Title)) + { + return attributes.AsEnumerable() + // keep Tags that are not already in Title + .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + // attributes concatenation, starting with Title + .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr)) + .ToString(); + } + + return string.Join(" ", attributes); } - if (!string.IsNullOrEmpty(ChannelLayout)) + case MediaStreamType.Video: { - attributes.Add(ChannelLayout); - } - else if (Channels.HasValue) - { - attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch"); - } - if (IsDefault) - { - attributes.Add("Default"); + var attributes = new List(); + + var resolutionText = GetResolutionText(); + + if (!string.IsNullOrEmpty(resolutionText)) + { + attributes.Add(resolutionText); + } + + if (!string.IsNullOrEmpty(Codec)) + { + attributes.Add(Codec.ToUpperInvariant()); + } + + if (!string.IsNullOrEmpty(Title)) + { + return attributes.AsEnumerable() + // keep Tags that are not already in Title + .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + // attributes concatenation, starting with Title + .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr)) + .ToString(); + } + + return string.Join(" ", attributes); } - return string.Join(" ", attributes); + case MediaStreamType.Subtitle: + { + var attributes = new List(); + + if (!string.IsNullOrEmpty(Language)) + { + attributes.Add(StringHelper.FirstToUpper(Language)); + } + else + { + attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); + } + + if (IsDefault) + { + attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + } + + if (IsForced) + { + attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); + } + + if (!string.IsNullOrEmpty(Title)) + { + return attributes.AsEnumerable() + // keep Tags that are not already in Title + .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + // attributes concatenation, starting with Title + .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr)) + .ToString(); + } + + return string.Join(" - ", attributes.ToArray()); + } + + default: + return null; } - - if (Type == MediaStreamType.Video) - { - var attributes = new List(); - - var resolutionText = GetResolutionText(); - - if (!string.IsNullOrEmpty(resolutionText)) - { - attributes.Add(resolutionText); - } - - if (!string.IsNullOrEmpty(Codec)) - { - attributes.Add(Codec.ToUpperInvariant()); - } - - return string.Join(" ", attributes); - } - - if (Type == MediaStreamType.Subtitle) - { - - var attributes = new List(); - - if (!string.IsNullOrEmpty(Language)) - { - attributes.Add(StringHelper.FirstToUpper(Language)); - } - else - { - attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); - } - - if (IsDefault) - { - attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); - } - - if (IsForced) - { - attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); - } - - if (!string.IsNullOrEmpty(Title)) - { - return attributes.AsEnumerable() - // keep Tags that are not already in Title - .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) - // attributes concatenation, starting with Title - .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr)) - .ToString(); - } - - return string.Join(" - ", attributes.ToArray()); - } - - if (Type == MediaStreamType.Video) - { - - } - - return null; } } From d85ca5276b0ab7848575a14b027d7a3e84f07b54 Mon Sep 17 00:00:00 2001 From: Pika Date: Wed, 8 Apr 2020 18:40:38 -0400 Subject: [PATCH 002/724] Port changes from #2773 --- MediaBrowser.Model/Entities/MediaStream.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 68e0242a97..f96c4f7e09 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -85,7 +85,12 @@ namespace MediaBrowser.Model.Entities if (!string.IsNullOrEmpty(Language)) { - attributes.Add(StringHelper.FirstToUpper(Language)); + // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + string fullLanguage = CultureInfo + .GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) + ?.DisplayName; + attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); } if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) @@ -99,7 +104,7 @@ namespace MediaBrowser.Model.Entities if (!string.IsNullOrEmpty(ChannelLayout)) { - attributes.Add(ChannelLayout); + attributes.Add(StringHelper.FirstToUpper(ChannelLayout)); } else if (Channels.HasValue) { @@ -121,7 +126,7 @@ namespace MediaBrowser.Model.Entities .ToString(); } - return string.Join(" ", attributes); + return string.Join(" - ", attributes); } case MediaStreamType.Video: From 7aba10eff67151a9f6593e9d3d702f17029b994f Mon Sep 17 00:00:00 2001 From: Pika Date: Wed, 8 Apr 2020 18:50:25 -0400 Subject: [PATCH 003/724] Forgot one --- MediaBrowser.Model/Entities/MediaStream.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index f96c4f7e09..be9c396778 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -164,7 +164,12 @@ namespace MediaBrowser.Model.Entities if (!string.IsNullOrEmpty(Language)) { - attributes.Add(StringHelper.FirstToUpper(Language)); + // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + string fullLanguage = CultureInfo + .GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) + ?.DisplayName; + attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); } else { From 1180b9746fe7c4a6562baff77910819a6706510b Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 15 Apr 2020 00:01:31 -0600 Subject: [PATCH 004/724] Migrates the notifications service to use ASP.NET MVC framework --- .../Api/NotificationsService.cs | 189 ------------------ .../Controllers/NotificationsController.cs | 138 +++++++++++++ .../NotificationDtos/NotificationDto.cs | 51 +++++ .../NotificationsSummaryDto.cs | 20 ++ .../ApiServiceCollectionExtensions.cs | 1 + 5 files changed, 210 insertions(+), 189 deletions(-) delete mode 100644 Emby.Notifications/Api/NotificationsService.cs create mode 100644 Jellyfin.Api/Controllers/NotificationsController.cs create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs deleted file mode 100644 index 788750796d..0000000000 --- a/Emby.Notifications/Api/NotificationsService.cs +++ /dev/null @@ -1,189 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1402 -#pragma warning disable SA1649 - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Notifications; -using MediaBrowser.Model.Services; - -namespace Emby.Notifications.Api -{ - [Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")] - public class GetNotifications : IReturn - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UserId { get; set; } = string.Empty; - - [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsRead { get; set; } - - [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; } - - [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; } - } - - public class Notification - { - public string Id { get; set; } = string.Empty; - - public string UserId { get; set; } = string.Empty; - - public DateTime Date { get; set; } - - public bool IsRead { get; set; } - - public string Name { get; set; } = string.Empty; - - public string Description { get; set; } = string.Empty; - - public string Url { get; set; } = string.Empty; - - public NotificationLevel Level { get; set; } - } - - public class NotificationResult - { - public IReadOnlyList Notifications { get; set; } = Array.Empty(); - - public int TotalRecordCount { get; set; } - } - - public class NotificationsSummary - { - public int UnreadCount { get; set; } - - public NotificationLevel MaxUnreadNotificationLevel { get; set; } - } - - [Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")] - public class GetNotificationsSummary : IReturn - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UserId { get; set; } = string.Empty; - } - - [Route("/Notifications/Types", "GET", Summary = "Gets notification types")] - public class GetNotificationTypes : IReturn> - { - } - - [Route("/Notifications/Services", "GET", Summary = "Gets notification types")] - public class GetNotificationServices : IReturn> - { - } - - [Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")] - public class AddAdminNotification : IReturnVoid - { - [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Name { get; set; } = string.Empty; - - [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Description { get; set; } = string.Empty; - - [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string? ImageUrl { get; set; } - - [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string? Url { get; set; } - - [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public NotificationLevel Level { get; set; } - } - - [Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")] - public class MarkRead : IReturnVoid - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } = string.Empty; - - [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } = string.Empty; - } - - [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")] - public class MarkUnread : IReturnVoid - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } = string.Empty; - - [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } = string.Empty; - } - - [Authenticated] - public class NotificationsService : IService - { - private readonly INotificationManager _notificationManager; - private readonly IUserManager _userManager; - - public NotificationsService(INotificationManager notificationManager, IUserManager userManager) - { - _notificationManager = notificationManager; - _userManager = userManager; - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotificationTypes request) - { - return _notificationManager.GetNotificationTypes(); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotificationServices request) - { - return _notificationManager.GetNotificationServices().ToList(); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotificationsSummary request) - { - return new NotificationsSummary - { - }; - } - - public Task Post(AddAdminNotification request) - { - // This endpoint really just exists as post of a real with sickbeard - var notification = new NotificationRequest - { - Date = DateTime.UtcNow, - Description = request.Description, - Level = request.Level, - Name = request.Name, - Url = request.Url, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray() - }; - - return _notificationManager.SendNotification(notification, CancellationToken.None); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public void Post(MarkRead request) - { - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public void Post(MarkUnread request) - { - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotifications request) - { - return new NotificationResult(); - } - } -} diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs new file mode 100644 index 0000000000..6602fca9c7 --- /dev/null +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -0,0 +1,138 @@ +#nullable enable +#pragma warning disable CA1801 +#pragma warning disable SA1313 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Jellyfin.Api.Models.NotificationDtos; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Notifications; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Notifications; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The notification controller. + /// + public class NotificationsController : BaseJellyfinApiController + { + private readonly INotificationManager _notificationManager; + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// The notification manager. + /// The user manager. + public NotificationsController(INotificationManager notificationManager, IUserManager userManager) + { + _notificationManager = notificationManager; + _userManager = userManager; + } + + /// + /// Endpoint for getting a user's notifications. + /// + /// The UserID. + /// An optional filter by IsRead. + /// The optional index to start at. All notifications with a lower index will be dropped from the results. + /// An optional limit on the number of notifications returned. + /// A read-only list of all of the user's notifications. + [HttpGet("{UserID}")] + public IReadOnlyList GetNotifications( + [FromRoute] string UserID, + [FromQuery] bool? IsRead, + [FromQuery] int? StartIndex, + [FromQuery] int? Limit) + { + return new List(); + } + + /// + /// Endpoint for getting a user's notification summary. + /// + /// The UserID. + /// Notifications summary for the user. + [HttpGet("{UserId}/Summary")] + public NotificationsSummaryDto GetNotificationsSummary( + [FromRoute] string UserID) + { + return new NotificationsSummaryDto(); + } + + /// + /// Endpoint for getting notification types. + /// + /// All notification types. + [HttpGet("Types")] + public IEnumerable GetNotificationTypes() + { + return _notificationManager.GetNotificationTypes(); + } + + /// + /// Endpoint for getting notification services. + /// + /// All notification services. + [HttpGet("Services")] + public IEnumerable GetNotificationServices() + { + return _notificationManager.GetNotificationServices(); + } + + /// + /// Endpoint to send a notification to all admins. + /// + /// The name of the notification. + /// The description of the notification. + /// The URL of the notification. + /// The level of the notification. + [HttpPost("Admin")] + public void CreateAdminNotification( + [FromForm] string Name, + [FromForm] string Description, + [FromForm] string? URL, + [FromForm] NotificationLevel Level) + { + var notification = new NotificationRequest + { + Name = Name, + Description = Description, + Url = URL, + Level = Level, + UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), + Date = DateTime.UtcNow, + }; + + _notificationManager.SendNotification(notification, CancellationToken.None); + } + + /// + /// Endpoint to set notifications as read. + /// + /// The UserID. + /// The IDs of notifications which should be set as read. + [HttpPost("{UserID}/Read")] + public void SetRead( + [FromRoute] string UserID, + [FromForm] List IDs) + { + } + + /// + /// Endpoint to set notifications as unread. + /// + /// The UserID. + /// The IDs of notifications which should be set as unread. + [HttpPost("{UserID}/Unread")] + public void SetUnread( + [FromRoute] string UserID, + [FromForm] List IDs) + { + } + } +} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs new file mode 100644 index 0000000000..7ecd2a49db --- /dev/null +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -0,0 +1,51 @@ +using System; +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Api.Models.NotificationDtos +{ + /// + /// The notification DTO. + /// + public class NotificationDto + { + /// + /// Gets or sets the notification ID. Defaults to an empty string. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the notification's user ID. Defaults to an empty string. + /// + public string UserId { get; set; } = string.Empty; + + /// + /// Gets or sets the notification date. + /// + public DateTime Date { get; set; } + + /// + /// Gets or sets a value indicating whether the notification has been read. + /// + public bool IsRead { get; set; } + + /// + /// Gets or sets the notification's name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the notification's description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the notification's URL. + /// + public string Url { get; set; } = string.Empty; + + /// + /// Gets or sets the notification level. + /// + public NotificationLevel Level { get; set; } + } +} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs new file mode 100644 index 0000000000..c18ab545d3 --- /dev/null +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs @@ -0,0 +1,20 @@ +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Api.Models.NotificationDtos +{ + /// + /// The notification summary DTO. + /// + public class NotificationsSummaryDto + { + /// + /// Gets or sets the number of unread notifications. + /// + public int UnreadCount { get; set; } + + /// + /// Gets or sets the maximum unread notification level. + /// + public NotificationLevel MaxUnreadNotificationLevel { get; set; } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dd4f9cd238..b3164e068f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -71,6 +71,7 @@ namespace Jellyfin.Server.Extensions // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) + .AddApplicationPart(typeof(NotificationsController).Assembly) .AddControllersAsServices(); } From ad1c880751dda93f1226e3846bb6a344ac58d0b6 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 15 Apr 2020 00:34:50 -0600 Subject: [PATCH 005/724] Lowercase parameters --- .../Controllers/NotificationsController.cs | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 6602fca9c7..31747584e1 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,6 +1,5 @@ #nullable enable #pragma warning disable CA1801 -#pragma warning disable SA1313 using System; using System.Collections.Generic; @@ -37,17 +36,17 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notifications. /// - /// The UserID. - /// An optional filter by IsRead. - /// The optional index to start at. All notifications with a lower index will be dropped from the results. - /// An optional limit on the number of notifications returned. + /// The UserID. + /// An optional filter by IsRead. + /// The optional index to start at. All notifications with a lower index will be dropped from the results. + /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] public IReadOnlyList GetNotifications( - [FromRoute] string UserID, - [FromQuery] bool? IsRead, - [FromQuery] int? StartIndex, - [FromQuery] int? Limit) + [FromRoute] string userID, + [FromQuery] bool? isRead, + [FromQuery] int? startIndex, + [FromQuery] int? limit) { return new List(); } @@ -55,11 +54,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notification summary. /// - /// The UserID. + /// The userID. /// Notifications summary for the user. - [HttpGet("{UserId}/Summary")] + [HttpGet("{UserID}/Summary")] public NotificationsSummaryDto GetNotificationsSummary( - [FromRoute] string UserID) + [FromRoute] string userID) { return new NotificationsSummaryDto(); } @@ -87,23 +86,23 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to send a notification to all admins. /// - /// The name of the notification. - /// The description of the notification. - /// The URL of the notification. - /// The level of the notification. + /// The name of the notification. + /// The description of the notification. + /// The URL of the notification. + /// The level of the notification. [HttpPost("Admin")] public void CreateAdminNotification( - [FromForm] string Name, - [FromForm] string Description, - [FromForm] string? URL, - [FromForm] NotificationLevel Level) + [FromForm] string name, + [FromForm] string description, + [FromForm] string? url, + [FromForm] NotificationLevel level) { var notification = new NotificationRequest { - Name = Name, - Description = Description, - Url = URL, - Level = Level, + Name = name, + Description = description, + Url = url, + Level = level, UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), Date = DateTime.UtcNow, }; @@ -114,24 +113,24 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to set notifications as read. /// - /// The UserID. - /// The IDs of notifications which should be set as read. + /// The userID. + /// The IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] public void SetRead( - [FromRoute] string UserID, - [FromForm] List IDs) + [FromRoute] string userID, + [FromForm] List ids) { } /// /// Endpoint to set notifications as unread. /// - /// The UserID. - /// The IDs of notifications which should be set as unread. + /// The userID. + /// The IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] public void SetUnread( - [FromRoute] string UserID, - [FromForm] List IDs) + [FromRoute] string userID, + [FromForm] List ids) { } } From 558b50a094adc82728a52b13862e19bc04783679 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 15 Apr 2020 09:29:29 -0600 Subject: [PATCH 006/724] Remove unnecessary assembly, update casing, enable nullable reference types on notification DTOs. --- .../Controllers/NotificationsController.cs | 20 +++++++++---------- .../NotificationDtos/NotificationDto.cs | 16 ++++++++------- .../NotificationsSummaryDto.cs | 4 +++- .../ApiServiceCollectionExtensions.cs | 1 - 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 31747584e1..c8a5be89b3 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -36,14 +36,14 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notifications. /// - /// The UserID. + /// The user's ID. /// An optional filter by IsRead. /// The optional index to start at. All notifications with a lower index will be dropped from the results. /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] public IReadOnlyList GetNotifications( - [FromRoute] string userID, + [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) @@ -54,11 +54,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notification summary. /// - /// The userID. + /// The user's ID. /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] public NotificationsSummaryDto GetNotificationsSummary( - [FromRoute] string userID) + [FromRoute] string userId) { return new NotificationsSummaryDto(); } @@ -95,14 +95,14 @@ namespace Jellyfin.Api.Controllers [FromForm] string name, [FromForm] string description, [FromForm] string? url, - [FromForm] NotificationLevel level) + [FromForm] NotificationLevel? level) { var notification = new NotificationRequest { Name = name, Description = description, Url = url, - Level = level, + Level = level ?? NotificationLevel.Normal, UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), Date = DateTime.UtcNow, }; @@ -113,11 +113,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to set notifications as read. /// - /// The userID. + /// The userID. /// The IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] public void SetRead( - [FromRoute] string userID, + [FromRoute] string userId, [FromForm] List ids) { } @@ -125,11 +125,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to set notifications as unread. /// - /// The userID. + /// The userID. /// The IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] public void SetUnread( - [FromRoute] string userID, + [FromRoute] string userId, [FromForm] List ids) { } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs index 7ecd2a49db..c849ecd75d 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using MediaBrowser.Model.Notifications; @@ -24,28 +26,28 @@ namespace Jellyfin.Api.Models.NotificationDtos public DateTime Date { get; set; } /// - /// Gets or sets a value indicating whether the notification has been read. + /// Gets or sets a value indicating whether the notification has been read. Defaults to false. /// - public bool IsRead { get; set; } + public bool IsRead { get; set; } = false; /// - /// Gets or sets the notification's name. + /// Gets or sets the notification's name. Defaults to an empty string. /// public string Name { get; set; } = string.Empty; /// - /// Gets or sets the notification's description. + /// Gets or sets the notification's description. Defaults to an empty string. /// public string Description { get; set; } = string.Empty; /// - /// Gets or sets the notification's URL. + /// Gets or sets the notification's URL. Defaults to null. /// - public string Url { get; set; } = string.Empty; + public string? Url { get; set; } /// /// Gets or sets the notification level. /// - public NotificationLevel Level { get; set; } + public NotificationLevel? Level { get; set; } } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs index c18ab545d3..b3746ee2da 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs @@ -1,3 +1,5 @@ +#nullable enable + using MediaBrowser.Model.Notifications; namespace Jellyfin.Api.Models.NotificationDtos @@ -15,6 +17,6 @@ namespace Jellyfin.Api.Models.NotificationDtos /// /// Gets or sets the maximum unread notification level. /// - public NotificationLevel MaxUnreadNotificationLevel { get; set; } + public NotificationLevel? MaxUnreadNotificationLevel { get; set; } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index b3164e068f..dd4f9cd238 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -71,7 +71,6 @@ namespace Jellyfin.Server.Extensions // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) - .AddApplicationPart(typeof(NotificationsController).Assembly) .AddControllersAsServices(); } From 8a7e4cd639be24eb58385dc7b36b466c3d6aed92 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 10:51:51 -0600 Subject: [PATCH 007/724] add redoc --- Jellyfin.Api/Jellyfin.Api.csproj | 3 ++- .../ApiApplicationBuilderExtensions.cs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 8f23ef9d03..cbb1d3007f 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -10,7 +10,8 @@ - + + diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index db06eb4552..2ab9b0ba5e 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -14,14 +14,18 @@ namespace Jellyfin.Server.Extensions /// The updated application builder. public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder) { - applicationBuilder.UseSwagger(); - // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. - return applicationBuilder.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1"); - }); + const string specEndpoint = "/swagger/v1/swagger.json"; + return applicationBuilder.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); + }) + .UseReDoc(c => + { + c.SpecUrl(specEndpoint); + }); } } } From e72a543570b59df61f48cb9a4049ab3dc9675250 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 11:24:03 -0600 Subject: [PATCH 008/724] Add Redoc, move docs to api-docs/ --- Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 2ab9b0ba5e..766243f201 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -21,10 +21,12 @@ namespace Jellyfin.Server.Extensions .UseSwaggerUI(c => { c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); + c.RoutePrefix = "api-docs/swagger"; }) .UseReDoc(c => { c.SpecUrl(specEndpoint); + c.RoutePrefix = "api-docs/redoc"; }); } } From 5da88fac4d0681126bdee635d59237d8d7fcebeb Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 11:24:32 -0600 Subject: [PATCH 009/724] Enable string enum converter --- .../ApiServiceCollectionExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 71ef9a69a2..a4f078b5b3 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; @@ -75,6 +80,9 @@ namespace Jellyfin.Server.Extensions { // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. options.JsonSerializerOptions.PropertyNamingPolicy = null; + + // Accept string enums + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }) .AddControllersAsServices(); } @@ -89,6 +97,17 @@ namespace Jellyfin.Server.Extensions return serviceCollection.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); + + // Add all xml doc files to swagger generator. + var xmlFiles = Directory.GetFiles( + AppContext.BaseDirectory, + "*.xml", + SearchOption.TopDirectoryOnly); + + foreach (var xmlFile in xmlFiles) + { + c.IncludeXmlComments(xmlFile); + } }); } } From 72745f47225a5b1071660acc4dcde618d938eaa0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 11:28:56 -0600 Subject: [PATCH 010/724] fix formatting --- Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 766243f201..43c49307d4 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -17,7 +17,8 @@ namespace Jellyfin.Server.Extensions // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. const string specEndpoint = "/swagger/v1/swagger.json"; - return applicationBuilder.UseSwagger() + return applicationBuilder + .UseSwagger() .UseSwaggerUI(c => { c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); From 86d68e23e7af367152edc36977a9a39431bd2641 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:06:18 -0600 Subject: [PATCH 011/724] Add DisplayPreferencesController --- .../Controllers/DisplayPreferencesController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Jellyfin.Api/Controllers/DisplayPreferencesController.cs diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs new file mode 100644 index 0000000000..537a940460 --- /dev/null +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Display Preferences Controller. + /// + public class DisplayPreferencesController : BaseJellyfinApiController + { + } +} From a282fbe9668263481b850b29b3fb8064d4d7ee9f Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:26:38 -0600 Subject: [PATCH 012/724] Move DisplayPreferences to Jellyfin.Api --- .../DisplayPreferencesController.cs | 92 ++++++++++++++++ MediaBrowser.Api/DisplayPreferencesService.cs | 101 ------------------ 2 files changed, 92 insertions(+), 101 deletions(-) delete mode 100644 MediaBrowser.Api/DisplayPreferencesService.cs diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 537a940460..6182c3507b 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,4 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -7,5 +14,90 @@ namespace Jellyfin.Api.Controllers /// public class DisplayPreferencesController : BaseJellyfinApiController { + private readonly IDisplayPreferencesRepository _displayPreferencesRepository; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository) + { + _displayPreferencesRepository = displayPreferencesRepository; + } + + /// + /// Get Display Preferences + /// + /// Display preferences id. + /// User id. + /// Client. + /// Display Preferences. + [HttpGet("{DisplayPreferencesId")] + [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetDisplayPreferences( + [FromRoute] string displayPreferencesId, + [FromQuery] [Required] string userId, + [FromQuery] [Required] string client + ) + { + try + { + var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); + if (result == null) + { + return NotFound(); + } + + // TODO ToOptimizedResult + return Ok(result); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Update Display Preferences + /// + /// Display preferences id. + /// User Id. + /// Client. + /// New Display Preferences object. + /// Status. + [HttpPost("{DisplayPreferencesId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult UpdateDisplayPreferences( + [FromRoute] string displayPreferencesId, + [FromQuery, BindRequired] string userId, + [FromQuery, BindRequired] string client, + [FromBody, BindRequired] DisplayPreferences displayPreferences) + { + try + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + displayPreferences.Id = displayPreferencesId; + _displayPreferencesRepository.SaveDisplayPreferences( + displayPreferences, + userId, + client, + CancellationToken.None); + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } } } diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs deleted file mode 100644 index 62c4ff43f2..0000000000 --- a/MediaBrowser.Api/DisplayPreferencesService.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Threading; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class UpdateDisplayPreferences - /// - [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")] - public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "DisplayPreferencesId", Description = "DisplayPreferences Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string DisplayPreferencesId { get; set; } - - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string UserId { get; set; } - } - - [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")] - public class GetDisplayPreferences : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string UserId { get; set; } - - [ApiMember(Name = "Client", Description = "Client", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Client { get; set; } - } - - /// - /// Class DisplayPreferencesService - /// - [Authenticated] - public class DisplayPreferencesService : BaseApiService - { - /// - /// The _display preferences manager - /// - private readonly IDisplayPreferencesRepository _displayPreferencesManager; - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// Initializes a new instance of the class. - /// - /// The json serializer. - /// The display preferences manager. - public DisplayPreferencesService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IJsonSerializer jsonSerializer, - IDisplayPreferencesRepository displayPreferencesManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _jsonSerializer = jsonSerializer; - _displayPreferencesManager = displayPreferencesManager; - } - - /// - /// Gets the specified request. - /// - /// The request. - public object Get(GetDisplayPreferences request) - { - var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client); - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(UpdateDisplayPreferences request) - { - // Serialize to json and then back so that the core doesn't see the request dto type - var displayPreferences = _jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(request)); - - _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None); - } - } -} From c31b9f5169ae62787fa356ccecc2f1fc6896d04b Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:30:10 -0600 Subject: [PATCH 013/724] Fix build & runtime errors --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 6182c3507b..a3bcafaea5 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -26,21 +26,20 @@ namespace Jellyfin.Api.Controllers } /// - /// Get Display Preferences + /// Get Display Preferences. /// /// Display preferences id. /// User id. /// Client. /// Display Preferences. - [HttpGet("{DisplayPreferencesId")] + [HttpGet("{DisplayPreferencesId}")] [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult GetDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery] [Required] string userId, - [FromQuery] [Required] string client - ) + [FromQuery] [Required] string client) { try { @@ -60,7 +59,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Update Display Preferences + /// Update Display Preferences. /// /// Display preferences id. /// User Id. From 60607ab60c3051815179859adfd2a7182f9ceb9a Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:34:34 -0600 Subject: [PATCH 014/724] Fix saving DisplayPreferences --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index a3bcafaea5..2c4072b39f 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -84,7 +84,11 @@ namespace Jellyfin.Api.Controllers return BadRequest(ModelState); } - displayPreferences.Id = displayPreferencesId; + if (displayPreferencesId == null) + { + // do nothing. + } + _displayPreferencesRepository.SaveDisplayPreferences( displayPreferences, userId, From e6b873f2aeadd01ed4638148be857ddf45a33576 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:56:16 -0600 Subject: [PATCH 015/724] Fix missing attributes --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 2c4072b39f..0fbdcb6b80 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,6 +1,9 @@ +#nullable enable + using System; using System.ComponentModel.DataAnnotations; using System.Threading; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; @@ -12,6 +15,7 @@ namespace Jellyfin.Api.Controllers /// /// Display Preferences Controller. /// + [Authenticated] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesRepository _displayPreferencesRepository; From 7c8188194b5bf9b74413f25d471a212f1677f7ed Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Sun, 19 Apr 2020 13:19:15 -0600 Subject: [PATCH 016/724] Address PR comments, and revert changes that changed the API schema --- .../Controllers/NotificationsController.cs | 20 +++++++++--------- .../NotificationDtos/NotificationDto.cs | 6 +++--- .../NotificationDtos/NotificationResultDto.cs | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index c8a5be89b3..d9a5c5e316 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -42,13 +42,13 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - public IReadOnlyList GetNotifications( + public NotificationResultDto GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) { - return new List(); + return new NotificationResultDto(); } /// @@ -92,10 +92,10 @@ namespace Jellyfin.Api.Controllers /// The level of the notification. [HttpPost("Admin")] public void CreateAdminNotification( - [FromForm] string name, - [FromForm] string description, - [FromForm] string? url, - [FromForm] NotificationLevel? level) + [FromQuery] string name, + [FromQuery] string description, + [FromQuery] string? url, + [FromQuery] NotificationLevel? level) { var notification = new NotificationRequest { @@ -114,11 +114,11 @@ namespace Jellyfin.Api.Controllers /// Endpoint to set notifications as read. /// /// The userID. - /// The IDs of notifications which should be set as read. + /// A comma-separated list of the IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] public void SetRead( [FromRoute] string userId, - [FromForm] List ids) + [FromQuery] string ids) { } @@ -126,11 +126,11 @@ namespace Jellyfin.Api.Controllers /// Endpoint to set notifications as unread. /// /// The userID. - /// The IDs of notifications which should be set as unread. + /// A comma-separated list of the IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] public void SetUnread( [FromRoute] string userId, - [FromForm] List ids) + [FromQuery] string ids) { } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs index c849ecd75d..502b22623b 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -41,13 +41,13 @@ namespace Jellyfin.Api.Models.NotificationDtos public string Description { get; set; } = string.Empty; /// - /// Gets or sets the notification's URL. Defaults to null. + /// Gets or sets the notification's URL. Defaults to an empty string. /// - public string? Url { get; set; } + public string Url { get; set; } = string.Empty; /// /// Gets or sets the notification level. /// - public NotificationLevel? Level { get; set; } + public NotificationLevel Level { get; set; } } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs new file mode 100644 index 0000000000..64e92bd83a --- /dev/null +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Api.Models.NotificationDtos +{ + /// + /// A list of notifications with the total record count for pagination. + /// + public class NotificationResultDto + { + /// + /// Gets or sets the current page of notifications. + /// + public IReadOnlyList Notifications { get; set; } = Array.Empty(); + + /// + /// Gets or sets the total number of notifications. + /// + public int TotalRecordCount { get; set; } + } +} From 5d9c40ec72d31957cec48e141ca5ce4f9141b413 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 16:26:20 -0600 Subject: [PATCH 017/724] move scheduled tasks to Jellyfin.Api --- .../Controllers/ScheduledTasksController.cs | 207 ++++++++++++++++++ .../Converters/LongToStringConverter.cs | 56 +++++ .../ApiServiceCollectionExtensions.cs | 2 + 3 files changed, 265 insertions(+) create mode 100644 Jellyfin.Api/Controllers/ScheduledTasksController.cs create mode 100644 Jellyfin.Server/Converters/LongToStringConverter.cs diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs new file mode 100644 index 0000000000..bb07af3979 --- /dev/null +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -0,0 +1,207 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Scheduled Tasks Controller. + /// + public class ScheduledTasksController : BaseJellyfinApiController + { + private readonly ITaskManager _taskManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public ScheduledTasksController(ITaskManager taskManager) + { + _taskManager = taskManager; + } + + /// + /// Get tasks. + /// + /// Optional filter tasks that are hidden, or not. + /// Optional filter tasks that are enabled, or not. + /// Task list. + [HttpGet] + [ProducesResponseType(typeof(TaskInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetTasks( + [FromQuery] bool? isHidden = false, + [FromQuery] bool? isEnabled = false) + { + try + { + IEnumerable tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); + + if (isHidden.HasValue) + { + var hiddenValue = isHidden.Value; + tasks = tasks.Where(o => + { + var itemIsHidden = false; + if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) + { + itemIsHidden = configurableScheduledTask.IsHidden; + } + + return itemIsHidden == hiddenValue; + }); + } + + if (isEnabled.HasValue) + { + var enabledValue = isEnabled.Value; + tasks = tasks.Where(o => + { + var itemIsEnabled = false; + if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) + { + itemIsEnabled = configurableScheduledTask.IsEnabled; + } + + return itemIsEnabled == enabledValue; + }); + } + + var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo); + + // TODO ToOptimizedResult + return Ok(taskInfos); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get task by id. + /// + /// Task Id. + /// Task Info. + [HttpGet("{TaskID}")] + [ProducesResponseType(typeof(TaskInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetTask([FromRoute] string taskId) + { + try + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(i => + string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); + + if (task == null) + { + return NotFound(); + } + + var result = ScheduledTaskHelpers.GetTaskInfo(task); + return Ok(result); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Start specified task. + /// + /// Task Id. + /// Status. + [HttpPost("Running/{TaskID}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult StartTask([FromRoute] string taskId) + { + try + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + + if (task == null) + { + return NotFound(); + } + + _taskManager.Execute(task, new TaskOptions()); + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Stop specified task. + /// + /// Task Id. + /// Status. + [HttpDelete("Running/{TaskID}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult StopTask([FromRoute] string taskId) + { + try + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + + if (task == null) + { + return NotFound(); + } + + _taskManager.Cancel(task); + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Update specified task triggers. + /// + /// Task Id. + /// Triggers. + /// Status. + [HttpPost("{TaskID}/Triggers")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult UpdateTask([FromRoute] string taskId, [FromBody] TaskTriggerInfo[] triggerInfos) + { + try + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + if (task == null) + { + return NotFound(); + } + + task.Triggers = triggerInfos; + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + } +} diff --git a/Jellyfin.Server/Converters/LongToStringConverter.cs b/Jellyfin.Server/Converters/LongToStringConverter.cs new file mode 100644 index 0000000000..ad66b7b0c3 --- /dev/null +++ b/Jellyfin.Server/Converters/LongToStringConverter.cs @@ -0,0 +1,56 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Server.Converters +{ + /// + /// Long to String JSON converter. + /// Javascript does not support 64-bit integers. + /// + public class LongToStringConverter : JsonConverter + { + /// + /// Read JSON string as Long. + /// + /// . + /// Type. + /// Options. + /// Parsed value. + public override long Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + // try to parse number directly from bytes + ReadOnlySpan span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) && span.Length == bytesConsumed) + { + return number; + } + + // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters + if (long.TryParse(reader.GetString(), out number)) + { + return number; + } + } + + // fallback to default handling + return reader.GetInt64(); + } + + /// + /// Write long to JSON string. + /// + /// . + /// Value to write. + /// Options. + public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(NumberFormatInfo.InvariantInfo)); + } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 71ef9a69a2..afd42ac5ac 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Server.Converters; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -75,6 +76,7 @@ namespace Jellyfin.Server.Extensions { // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. options.JsonSerializerOptions.PropertyNamingPolicy = null; + options.JsonSerializerOptions.Converters.Add(new LongToStringConverter()); }) .AddControllersAsServices(); } From d8fc4f91dbcc38df0e13e51a3631e87f783361de Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 16:29:29 -0600 Subject: [PATCH 018/724] burn ToOptimizedResult --- Jellyfin.Api/Controllers/ScheduledTasksController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index bb07af3979..f90b449673 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -75,7 +75,6 @@ namespace Jellyfin.Api.Controllers var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo); - // TODO ToOptimizedResult return Ok(taskInfos); } catch (Exception e) From 4a960892c20676ce6400f4cae1c85e8ce4d4a841 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 16:31:09 -0600 Subject: [PATCH 019/724] Add Authorize and BindRequired --- Jellyfin.Api/Controllers/ScheduledTasksController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index f90b449673..157e985197 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -13,6 +14,7 @@ namespace Jellyfin.Api.Controllers /// /// Scheduled Tasks Controller. /// + [Authenticated] public class ScheduledTasksController : BaseJellyfinApiController { private readonly ITaskManager _taskManager; @@ -183,7 +185,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult UpdateTask([FromRoute] string taskId, [FromBody] TaskTriggerInfo[] triggerInfos) + public IActionResult UpdateTask([FromRoute] string taskId, [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos) { try { From a96db5f48e57a192369b220422517171c06411b6 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 16:32:03 -0600 Subject: [PATCH 020/724] Remove old scheduled tasks service --- .../ScheduledTasks/ScheduledTaskService.cs | 234 ------------------ 1 file changed, 234 deletions(-) delete mode 100644 MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs deleted file mode 100644 index e08a8482e0..0000000000 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.ScheduledTasks -{ - /// - /// Class GetScheduledTask - /// - [Route("/ScheduledTasks/{Id}", "GET", Summary = "Gets a scheduled task, by Id")] - public class GetScheduledTask : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - /// - /// Class GetScheduledTasks - /// - [Route("/ScheduledTasks", "GET", Summary = "Gets scheduled tasks")] - public class GetScheduledTasks : IReturn - { - [ApiMember(Name = "IsHidden", Description = "Optional filter tasks that are hidden, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsHidden { get; set; } - - [ApiMember(Name = "IsEnabled", Description = "Optional filter tasks that are enabled, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsEnabled { get; set; } - } - - /// - /// Class StartScheduledTask - /// - [Route("/ScheduledTasks/Running/{Id}", "POST", Summary = "Starts a scheduled task")] - public class StartScheduledTask : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - /// - /// Class StopScheduledTask - /// - [Route("/ScheduledTasks/Running/{Id}", "DELETE", Summary = "Stops a scheduled task")] - public class StopScheduledTask : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - /// - /// Class UpdateScheduledTaskTriggers - /// - [Route("/ScheduledTasks/{Id}/Triggers", "POST", Summary = "Updates the triggers for a scheduled task")] - public class UpdateScheduledTaskTriggers : List, IReturnVoid - { - /// - /// Gets or sets the task id. - /// - /// The task id. - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - /// - /// Class ScheduledTasksService - /// - [Authenticated(Roles = "Admin")] - public class ScheduledTaskService : BaseApiService - { - /// - /// The task manager. - /// - private readonly ITaskManager _taskManager; - - /// - /// Initializes a new instance of the class. - /// - /// The task manager. - /// taskManager - public ScheduledTaskService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ITaskManager taskManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _taskManager = taskManager; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// IEnumerable{TaskInfo}. - public object Get(GetScheduledTasks request) - { - IEnumerable result = _taskManager.ScheduledTasks - .OrderBy(i => i.Name); - - if (request.IsHidden.HasValue) - { - var val = request.IsHidden.Value; - - result = result.Where(i => - { - var isHidden = false; - - if (i.ScheduledTask is IConfigurableScheduledTask configurableTask) - { - isHidden = configurableTask.IsHidden; - } - - return isHidden == val; - }); - } - - if (request.IsEnabled.HasValue) - { - var val = request.IsEnabled.Value; - - result = result.Where(i => - { - var isEnabled = true; - - if (i.ScheduledTask is IConfigurableScheduledTask configurableTask) - { - isEnabled = configurableTask.IsEnabled; - } - - return isEnabled == val; - }); - } - - var infos = result - .Select(ScheduledTaskHelpers.GetTaskInfo) - .ToArray(); - - return ToOptimizedResult(infos); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// IEnumerable{TaskInfo}. - /// Task not found - public object Get(GetScheduledTask request) - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id)); - - if (task == null) - { - throw new ResourceNotFoundException("Task not found"); - } - - var result = ScheduledTaskHelpers.GetTaskInfo(task); - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified request. - /// - /// The request. - /// Task not found - public void Post(StartScheduledTask request) - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id)); - - if (task == null) - { - throw new ResourceNotFoundException("Task not found"); - } - - _taskManager.Execute(task, new TaskOptions()); - } - - /// - /// Posts the specified request. - /// - /// The request. - /// Task not found - public void Delete(StopScheduledTask request) - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id)); - - if (task == null) - { - throw new ResourceNotFoundException("Task not found"); - } - - _taskManager.Cancel(task); - } - - /// - /// Posts the specified request. - /// - /// The request. - /// Task not found - public void Post(UpdateScheduledTaskTriggers request) - { - // We need to parse this manually because we told service stack not to with IRequiresRequestStream - // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs - var id = GetPathValue(1).ToString(); - - var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.Ordinal)); - - if (task == null) - { - throw new ResourceNotFoundException("Task not found"); - } - - task.Triggers = request.ToArray(); - } - } -} From c5d709f77ed2158bf68b8cc81238067d4525518f Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 16:35:31 -0600 Subject: [PATCH 021/724] remove todo --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0fbdcb6b80..0554091b45 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -53,7 +53,6 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - // TODO ToOptimizedResult return Ok(result); } catch (Exception e) From a41d5fcea4ee082bb49ddac34a1606204e12e8e8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 17:36:05 -0600 Subject: [PATCH 022/724] 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 023/724] 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 ad67081840ec61085673634795d0b6363f649dbf Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 18:04:36 -0600 Subject: [PATCH 024/724] add camelCase formatter --- .../CamelCaseJsonProfileFormatter.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs new file mode 100644 index 0000000000..433a3197d3 --- /dev/null +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Formatters +{ + /// + /// Camel Case Json Profile Formatter. + /// + public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter + { + /// + /// Initializes a new instance of the class. + /// + public CamelCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) + { + SupportedMediaTypes.Clear(); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); + } + } +} From c89dc8921ffb0ce11031e9cfb096b525d94e21b3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 18:10:59 -0600 Subject: [PATCH 025/724] Fix PascalCase --- .../ApiServiceCollectionExtensions.cs | 3 +++ .../PascalCaseJsonProfileFormatter.cs | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 71ef9a69a2..00688074f0 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Server.Formatters; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -66,6 +67,8 @@ namespace Jellyfin.Server.Extensions return serviceCollection.AddMvc(opts => { opts.UseGeneralRoutePrefix(baseUrl); + opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); + opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); }) // Clear app parts to avoid other assemblies being picked up diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs new file mode 100644 index 0000000000..2ed006a336 --- /dev/null +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Formatters +{ + /// + /// Pascal Case Json Profile Formatter. + /// + public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter + { + /// + /// Initializes a new instance of the class. + /// + public PascalCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = null }) + { + SupportedMediaTypes.Clear(); + // Add application/json for default formatter + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\"")); + } + } +} From 21b54b4ad8477d654e4f79e9805701c9737346a6 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 19:33:55 -0600 Subject: [PATCH 026/724] 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 027/724] 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 16cae23bbee14a7398d39014973b1a476e1ca57c Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Sun, 19 Apr 2020 21:06:28 -0600 Subject: [PATCH 028/724] Add response type annotations, return IActionResult to handle errors --- .../Controllers/NotificationsController.cs | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index d9a5c5e316..76b025fa16 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Notifications; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers @@ -42,13 +43,14 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - public NotificationResultDto GetNotifications( + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public IActionResult GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) { - return new NotificationResultDto(); + return Ok(new NotificationResultDto()); } /// @@ -57,10 +59,11 @@ namespace Jellyfin.Api.Controllers /// The user's ID. /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] - public NotificationsSummaryDto GetNotificationsSummary( + [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)] + public IActionResult GetNotificationsSummary( [FromRoute] string userId) { - return new NotificationsSummaryDto(); + return Ok(new NotificationsSummaryDto()); } /// @@ -68,9 +71,18 @@ namespace Jellyfin.Api.Controllers /// /// All notification types. [HttpGet("Types")] - public IEnumerable GetNotificationTypes() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetNotificationTypes() { - return _notificationManager.GetNotificationTypes(); + try + { + return Ok(_notificationManager.GetNotificationTypes()); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } } /// @@ -78,9 +90,18 @@ namespace Jellyfin.Api.Controllers /// /// All notification services. [HttpGet("Services")] - public IEnumerable GetNotificationServices() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetNotificationServices() { - return _notificationManager.GetNotificationServices(); + try + { + return Ok(_notificationManager.GetNotificationServices()); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } } /// @@ -90,24 +111,36 @@ namespace Jellyfin.Api.Controllers /// The description of the notification. /// The URL of the notification. /// The level of the notification. + /// Status. [HttpPost("Admin")] - public void CreateAdminNotification( + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, [FromQuery] string? url, [FromQuery] NotificationLevel? level) { - var notification = new NotificationRequest + try { - Name = name, - Description = description, - Url = url, - Level = level ?? NotificationLevel.Normal, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), - Date = DateTime.UtcNow, - }; + var notification = new NotificationRequest + { + Name = name, + Description = description, + Url = url, + Level = level ?? NotificationLevel.Normal, + UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), + Date = DateTime.UtcNow, + }; - _notificationManager.SendNotification(notification, CancellationToken.None); + _notificationManager.SendNotification(notification, CancellationToken.None); + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } } /// @@ -115,11 +148,14 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. + /// Status. [HttpPost("{UserID}/Read")] - public void SetRead( + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult SetRead( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } /// @@ -127,11 +163,14 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. + /// Status. [HttpPost("{UserID}/Unread")] - public void SetUnread( + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult SetUnread( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } } } From 688240151bae0f333cd329572b3774954d13ebae Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Mon, 20 Apr 2020 00:00:00 -0600 Subject: [PATCH 029/724] Enable nullable reference types on new class, remove unnecessary documenation and return types --- Jellyfin.Api/Controllers/NotificationsController.cs | 11 ++--------- .../Models/NotificationDtos/NotificationResultDto.cs | 2 ++ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 76b025fa16..c0c2be626b 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -72,7 +72,6 @@ namespace Jellyfin.Api.Controllers /// All notification types. [HttpGet("Types")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetNotificationTypes() { try @@ -91,7 +90,6 @@ namespace Jellyfin.Api.Controllers /// All notification services. [HttpGet("Services")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetNotificationServices() { try @@ -114,7 +112,6 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, @@ -148,14 +145,12 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. - /// Status. [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult SetRead( + public void SetRead( [FromRoute] string userId, [FromQuery] string ids) { - return Ok(); } /// @@ -163,14 +158,12 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. - /// Status. [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult SetUnread( + public void SetUnread( [FromRoute] string userId, [FromQuery] string ids) { - return Ok(); } } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs index 64e92bd83a..e34e176cb9 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; From fff2a40ffc4e5010b26143185c68d221225c1a22 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 20 Apr 2020 07:24:13 -0600 Subject: [PATCH 030/724] Remove StringEnumConverter --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a4f078b5b3..92bacb4400 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -80,9 +80,6 @@ namespace Jellyfin.Server.Extensions { // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. options.JsonSerializerOptions.PropertyNamingPolicy = null; - - // Accept string enums - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }) .AddControllersAsServices(); } From e151d539f2041fb249af82118bde1168d1859c6b Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 20 Apr 2020 13:06:29 -0600 Subject: [PATCH 031/724] Move ImageByNameService to Jellyfin.Api --- .../Images/ImageByNameController.cs | 261 +++++++++++++++++ MediaBrowser.Api/Images/ImageByNameService.cs | 277 ------------------ 2 files changed, 261 insertions(+), 277 deletions(-) create mode 100644 Jellyfin.Api/Controllers/Images/ImageByNameController.cs delete mode 100644 MediaBrowser.Api/Images/ImageByNameService.cs diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs new file mode 100644 index 0000000000..a14e2403c4 --- /dev/null +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -0,0 +1,261 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers.Images +{ + /// + /// Images By Name Controller. + /// + [Route("Images")] + [Authenticated] + public class ImageByNameController : BaseJellyfinApiController + { + private readonly IServerApplicationPaths _applicationPaths; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public ImageByNameController( + IServerConfigurationManager serverConfigurationManager, + IFileSystem fileSystem) + { + _applicationPaths = serverConfigurationManager.ApplicationPaths; + _fileSystem = fileSystem; + } + + /// + /// Get all general images. + /// + /// General images. + [HttpGet("General")] + [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetGeneralImages() + { + try + { + return Ok(GetImageList(_applicationPaths.GeneralPath, false)); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get General Image. + /// + /// The name of the image. + /// Image Type (primary, backdrop, logo, etc). + /// Image Stream. + [HttpGet("General/{Name}/{Type}")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type) + { + try + { + var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) + ? "folder" + : type; + + var paths = BaseItem.SupportedImageExtensions + .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList(); + + var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault(); + if (path == null || !System.IO.File.Exists(path)) + { + return NotFound(); + } + + var contentType = MimeTypes.GetMimeType(path); + return new FileStreamResult(System.IO.File.OpenRead(path), contentType); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get all general images. + /// + /// General images. + [HttpGet("Ratings")] + [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetRatingImages() + { + try + { + return Ok(GetImageList(_applicationPaths.RatingsPath, false)); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get rating image. + /// + /// The theme to get the image from. + /// The name of the image. + /// Image Stream. + [HttpGet("Ratings/{Theme}/{Name}")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetRatingImage( + [FromRoute] string theme, + [FromRoute] string name) + { + try + { + return GetImageFile(_applicationPaths.RatingsPath, theme, name); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get all media info images. + /// + /// Media Info images. + [HttpGet("MediaInfo")] + [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetMediaInfoImages() + { + try + { + return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false)); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get media info image. + /// + /// The theme to get the image from. + /// The name of the image. + /// Image Stream. + [HttpGet("MediaInfo/{Theme}/{Name}")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetMediaInfoImage( + [FromRoute] string theme, + [FromRoute] string name) + { + try + { + return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Internal FileHelper. + /// + /// Path to begin search. + /// Theme to search. + /// File name to search for. + /// Image Stream. + private IActionResult GetImageFile(string basePath, string theme, string name) + { + var themeFolder = Path.Combine(basePath, theme); + if (Directory.Exists(themeFolder)) + { + var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) + .FirstOrDefault(System.IO.File.Exists); + + if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) + { + var contentType = MimeTypes.GetMimeType(path); + return new FileStreamResult(System.IO.File.OpenRead(path), contentType); + } + } + + var allFolder = Path.Combine(basePath, "all"); + if (Directory.Exists(allFolder)) + { + var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) + .FirstOrDefault(System.IO.File.Exists); + + if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) + { + var contentType = MimeTypes.GetMimeType(path); + return new FileStreamResult(System.IO.File.OpenRead(path), contentType); + } + } + + return NotFound(); + } + + private List GetImageList(string path, bool supportsThemes) + { + try + { + return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true) + .Select(i => new ImageByNameInfo + { + Name = _fileSystem.GetFileNameWithoutExtension(i), + FileLength = i.Length, + + // For themeable images, use the Theme property + // For general images, the same object structure is fine, + // but it's not owned by a theme, so call it Context + Theme = supportsThemes ? GetThemeName(i.FullName, path) : null, + Context = supportsThemes ? null : GetThemeName(i.FullName, path), + Format = i.Extension.ToLowerInvariant().TrimStart('.') + }) + .OrderBy(i => i.Name) + .ToList(); + } + catch (IOException) + { + return new List(); + } + } + + private string GetThemeName(string path, string rootImagePath) + { + var parentName = Path.GetDirectoryName(path); + + if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + parentName = Path.GetFileName(parentName); + + return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName; + } + } +} diff --git a/MediaBrowser.Api/Images/ImageByNameService.cs b/MediaBrowser.Api/Images/ImageByNameService.cs deleted file mode 100644 index 45b7d0c100..0000000000 --- a/MediaBrowser.Api/Images/ImageByNameService.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Images -{ - /// - /// Class GetGeneralImage - /// - [Route("/Images/General/{Name}/{Type}", "GET", Summary = "Gets a general image by name")] - public class GetGeneralImage - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - [ApiMember(Name = "Type", Description = "Image Type (primary, backdrop, logo, etc).", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Type { get; set; } - } - - /// - /// Class GetRatingImage - /// - [Route("/Images/Ratings/{Theme}/{Name}", "GET", Summary = "Gets a rating image by name")] - public class GetRatingImage - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - /// - /// Gets or sets the theme. - /// - /// The theme. - [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Theme { get; set; } - } - - /// - /// Class GetMediaInfoImage - /// - [Route("/Images/MediaInfo/{Theme}/{Name}", "GET", Summary = "Gets a media info image by name")] - public class GetMediaInfoImage - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - /// - /// Gets or sets the theme. - /// - /// The theme. - [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Theme { get; set; } - } - - [Route("/Images/MediaInfo", "GET", Summary = "Gets all media info image by name")] - [Authenticated] - public class GetMediaInfoImages : IReturn> - { - } - - [Route("/Images/Ratings", "GET", Summary = "Gets all rating images by name")] - [Authenticated] - public class GetRatingImages : IReturn> - { - } - - [Route("/Images/General", "GET", Summary = "Gets all general images by name")] - [Authenticated] - public class GetGeneralImages : IReturn> - { - } - - /// - /// Class ImageByNameService - /// - public class ImageByNameService : BaseApiService - { - /// - /// The _app paths - /// - private readonly IServerApplicationPaths _appPaths; - - private readonly IFileSystem _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - public ImageByNameService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory resultFactory, - IFileSystem fileSystem) - : base(logger, serverConfigurationManager, resultFactory) - { - _appPaths = serverConfigurationManager.ApplicationPaths; - _fileSystem = fileSystem; - } - - public object Get(GetMediaInfoImages request) - { - return ToOptimizedResult(GetImageList(_appPaths.MediaInfoImagesPath, true)); - } - - public object Get(GetRatingImages request) - { - return ToOptimizedResult(GetImageList(_appPaths.RatingsPath, true)); - } - - public object Get(GetGeneralImages request) - { - return ToOptimizedResult(GetImageList(_appPaths.GeneralPath, false)); - } - - private List GetImageList(string path, bool supportsThemes) - { - try - { - return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true) - .Select(i => new ImageByNameInfo - { - Name = _fileSystem.GetFileNameWithoutExtension(i), - FileLength = i.Length, - - // For themeable images, use the Theme property - // For general images, the same object structure is fine, - // but it's not owned by a theme, so call it Context - Theme = supportsThemes ? GetThemeName(i.FullName, path) : null, - Context = supportsThemes ? null : GetThemeName(i.FullName, path), - - Format = i.Extension.ToLowerInvariant().TrimStart('.') - }) - .OrderBy(i => i.Name) - .ToList(); - } - catch (IOException) - { - return new List(); - } - } - - private string GetThemeName(string path, string rootImagePath) - { - var parentName = Path.GetDirectoryName(path); - - if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - parentName = Path.GetFileName(parentName); - - return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? - null : - parentName; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public Task Get(GetGeneralImage request) - { - var filename = string.Equals(request.Type, "primary", StringComparison.OrdinalIgnoreCase) - ? "folder" - : request.Type; - - var paths = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(_appPaths.GeneralPath, request.Name, filename + i)).ToList(); - - var path = paths.FirstOrDefault(File.Exists) ?? paths.FirstOrDefault(); - - return ResultFactory.GetStaticFileResult(Request, path); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetRatingImage request) - { - var themeFolder = Path.Combine(_appPaths.RatingsPath, request.Theme); - - if (Directory.Exists(themeFolder)) - { - var path = BaseItem.SupportedImageExtensions - .Select(i => Path.Combine(themeFolder, request.Name + i)) - .FirstOrDefault(File.Exists); - - if (!string.IsNullOrEmpty(path)) - { - return ResultFactory.GetStaticFileResult(Request, path); - } - } - - var allFolder = Path.Combine(_appPaths.RatingsPath, "all"); - - if (Directory.Exists(allFolder)) - { - // Avoid implicitly captured closure - var currentRequest = request; - - var path = BaseItem.SupportedImageExtensions - .Select(i => Path.Combine(allFolder, currentRequest.Name + i)) - .FirstOrDefault(File.Exists); - - if (!string.IsNullOrEmpty(path)) - { - return ResultFactory.GetStaticFileResult(Request, path); - } - } - - throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public Task Get(GetMediaInfoImage request) - { - var themeFolder = Path.Combine(_appPaths.MediaInfoImagesPath, request.Theme); - - if (Directory.Exists(themeFolder)) - { - var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, request.Name + i)) - .FirstOrDefault(File.Exists); - - if (!string.IsNullOrEmpty(path)) - { - return ResultFactory.GetStaticFileResult(Request, path); - } - } - - var allFolder = Path.Combine(_appPaths.MediaInfoImagesPath, "all"); - - if (Directory.Exists(allFolder)) - { - // Avoid implicitly captured closure - var currentRequest = request; - - var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i)) - .FirstOrDefault(File.Exists); - - if (!string.IsNullOrEmpty(path)) - { - return ResultFactory.GetStaticFileResult(Request, path); - } - } - - throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name); - } - } -} From 376619369d8b1e889475da1191092f43e7f26ae6 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 20 Apr 2020 13:12:35 -0600 Subject: [PATCH 032/724] fix build --- Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index a14e2403c4..3097296051 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers.Images } } - private string GetThemeName(string path, string rootImagePath) + private string? GetThemeName(string path, string rootImagePath) { var parentName = Path.GetDirectoryName(path); From 766d2ee413a15c682c0d687619064caf98f9031c Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 20 Apr 2020 14:21:06 -0600 Subject: [PATCH 033/724] Move RemoteImageService to Jellyfin.API --- .../Images/RemoteImageController.cs | 290 +++++++++++++++++ MediaBrowser.Api/Images/RemoteImageService.cs | 295 ------------------ 2 files changed, 290 insertions(+), 295 deletions(-) create mode 100644 Jellyfin.Api/Controllers/Images/RemoteImageController.cs delete mode 100644 MediaBrowser.Api/Images/RemoteImageService.cs diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs new file mode 100644 index 0000000000..66479582da --- /dev/null +++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs @@ -0,0 +1,290 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers.Images +{ + /// + /// Remote Images Controller. + /// + [Route("Images")] + [Authenticated] + public class RemoteImageController : BaseJellyfinApiController + { + private readonly IProviderManager _providerManager; + private readonly IServerApplicationPaths _applicationPaths; + private readonly IHttpClient _httpClient; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public RemoteImageController( + IProviderManager providerManager, + IServerApplicationPaths applicationPaths, + IHttpClient httpClient, + ILibraryManager libraryManager) + { + _providerManager = providerManager; + _applicationPaths = applicationPaths; + _httpClient = httpClient; + _libraryManager = libraryManager; + } + + /// + /// Gets available remote images for an item. + /// + /// Item Id. + /// The image type. + /// 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 image provider to use. + /// Optinal. Include all languages. + /// Remote Image Result. + [HttpGet("{Id}/RemoteImages")] + [ProducesResponseType(typeof(RemoteImageResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)] + public async Task GetRemoteImages( + [FromRoute] string id, + [FromQuery] ImageType? type, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string providerName, + [FromQuery] bool includeAllLanguages) + { + try + { + var item = _libraryManager.GetItemById(id); + if (item == null) + { + return NotFound(); + } + + var images = await _providerManager.GetAvailableRemoteImages( + item, + new RemoteImageQuery + { + ProviderName = providerName, + IncludeAllLanguages = includeAllLanguages, + IncludeDisabledProviders = true, + ImageType = type + }, CancellationToken.None) + .ConfigureAwait(false); + + var imageArray = images.ToArray(); + var allProviders = _providerManager.GetRemoteImageProviderInfo(item); + if (type.HasValue) + { + allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value)); + } + + var result = new RemoteImageResult + { + TotalRecordCount = imageArray.Length, + Providers = allProviders.Select(o => o.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() + }; + + if (startIndex.HasValue) + { + imageArray = imageArray.Skip(startIndex.Value).ToArray(); + } + + if (limit.HasValue) + { + imageArray = imageArray.Take(limit.Value).ToArray(); + } + + result.Images = imageArray; + return Ok(result); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Gets available remote image providers for an item. + /// + /// Item Id. + /// List of providers. + [HttpGet("{Id}/RemoteImages/Providers")] + [ProducesResponseType(typeof(ImageProviderInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetRemoteImageProviders([FromRoute] string id) + { + try + { + var item = _libraryManager.GetItemById(id); + if (item == null) + { + return NotFound(); + } + + var providers = _providerManager.GetRemoteImageProviderInfo(item); + return Ok(providers); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Gets a remote image. + /// + /// The image url. + /// Image Stream. + [HttpGet("Remote")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task GetRemoteImage([FromQuery, BindRequired] string imageUrl) + { + try + { + var urlHash = imageUrl.GetMD5(); + var pointerCachePath = GetFullCachePath(urlHash.ToString()); + + string? contentPath = null; + bool hasFile = false; + + try + { + contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); + if (System.IO.File.Exists(contentPath)) + { + hasFile = true; + } + } + catch (FileNotFoundException) + { + // Means the file isn't cached yet + } + catch (IOException) + { + // Means the file isn't cached yet + } + + if (!hasFile) + { + await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); + contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); + } + + if (string.IsNullOrEmpty(contentPath)) + { + return NotFound(); + } + + var contentType = MimeTypes.GetMimeType(contentPath); + return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Downloads a remote image for an item. + /// + /// Item Id. + /// The image type. + /// The image url. + /// Status. + [HttpPost("{Id}/RemoteImages/Download")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task DownloadRemoteImage( + [FromRoute] string id, + [FromQuery, BindRequired] ImageType type, + [FromQuery] string imageUrl) + { + try + { + var item = _libraryManager.GetItemById(id); + if (item == null) + { + return NotFound(); + } + + await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) + .ConfigureAwait(false); + + item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Gets the full cache path. + /// + /// The filename. + /// System.String. + private string GetFullCachePath(string filename) + { + return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); + } + + /// + /// Downloads the image. + /// + /// The URL. + /// The URL hash. + /// The pointer cache path. + /// Task. + private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) + { + using var result = await _httpClient.GetResponse(new HttpRequestOptions + { + Url = url, + BufferContent = false + }).ConfigureAwait(false); + var ext = result.ContentType.Split('/').Last(); + + var fullCachePath = GetFullCachePath(urlHash + "." + ext); + + Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); + using (var stream = result.Content) + { + using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await stream.CopyToAsync(filestream).ConfigureAwait(false); + } + + Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); + await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None) + .ConfigureAwait(false); + } + } +} diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs deleted file mode 100644 index 222bb34d31..0000000000 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ /dev/null @@ -1,295 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Images -{ - public class BaseRemoteImageRequest : IReturn - { - [ApiMember(Name = "Type", Description = "The image type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public ImageType? Type { get; set; } - - /// - /// 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 = "ProviderName", Description = "Optional. The image provider to use", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ProviderName { get; set; } - - [ApiMember(Name = "IncludeAllLanguages", Description = "Optional.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool IncludeAllLanguages { get; set; } - } - - [Route("/Items/{Id}/RemoteImages", "GET", Summary = "Gets available remote images for an item")] - [Authenticated] - public class GetRemoteImages : BaseRemoteImageRequest - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Items/{Id}/RemoteImages/Providers", "GET", Summary = "Gets available remote image providers for an item")] - [Authenticated] - public class GetRemoteImageProviders : IReturn> - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - public class BaseDownloadRemoteImage : IReturnVoid - { - [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET,POST")] - public ImageType Type { get; set; } - - [ApiMember(Name = "ProviderName", Description = "The image provider", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] - public string ProviderName { get; set; } - - [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] - public string ImageUrl { get; set; } - } - - [Route("/Items/{Id}/RemoteImages/Download", "POST", Summary = "Downloads a remote image for an item")] - [Authenticated(Roles = "Admin")] - public class DownloadRemoteImage : BaseDownloadRemoteImage - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Images/Remote", "GET", Summary = "Gets a remote image")] - public class GetRemoteImage - { - [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ImageUrl { get; set; } - } - - public class RemoteImageService : BaseApiService - { - private readonly IProviderManager _providerManager; - - private readonly IServerApplicationPaths _appPaths; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - - private readonly ILibraryManager _libraryManager; - - public RemoteImageService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IProviderManager providerManager, - IServerApplicationPaths appPaths, - IHttpClient httpClient, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _providerManager = providerManager; - _appPaths = appPaths; - _httpClient = httpClient; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - } - - public object Get(GetRemoteImageProviders request) - { - var item = _libraryManager.GetItemById(request.Id); - - var result = GetImageProviders(item); - - return ToOptimizedResult(result); - } - - private List GetImageProviders(BaseItem item) - { - return _providerManager.GetRemoteImageProviderInfo(item).ToList(); - } - - public async Task Get(GetRemoteImages request) - { - var item = _libraryManager.GetItemById(request.Id); - - var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery - { - ProviderName = request.ProviderName, - IncludeAllLanguages = request.IncludeAllLanguages, - IncludeDisabledProviders = true, - ImageType = request.Type - - }, CancellationToken.None).ConfigureAwait(false); - - var imagesList = images.ToArray(); - - var allProviders = _providerManager.GetRemoteImageProviderInfo(item); - - if (request.Type.HasValue) - { - allProviders = allProviders.Where(i => i.SupportedImages.Contains(request.Type.Value)); - } - - var result = new RemoteImageResult - { - TotalRecordCount = imagesList.Length, - Providers = allProviders.Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() - }; - - if (request.StartIndex.HasValue) - { - imagesList = imagesList.Skip(request.StartIndex.Value) - .ToArray(); - } - - if (request.Limit.HasValue) - { - imagesList = imagesList.Take(request.Limit.Value) - .ToArray(); - } - - result.Images = imagesList; - - return result; - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(DownloadRemoteImage request) - { - var item = _libraryManager.GetItemById(request.Id); - - return DownloadRemoteImage(item, request); - } - - /// - /// Downloads the remote image. - /// - /// The item. - /// The request. - /// Task. - private async Task DownloadRemoteImage(BaseItem item, BaseDownloadRemoteImage request) - { - await _providerManager.SaveImage(item, request.ImageUrl, request.Type, null, CancellationToken.None).ConfigureAwait(false); - - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task Get(GetRemoteImage request) - { - var urlHash = request.ImageUrl.GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string contentPath; - - try - { - contentPath = File.ReadAllText(pointerCachePath); - - if (File.Exists(contentPath)) - { - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - } - catch (FileNotFoundException) - { - // Means the file isn't cached yet - } - catch (IOException) - { - // Means the file isn't cached yet - } - - await DownloadImage(request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - - // Read the pointer file again - contentPath = File.ReadAllText(pointerCachePath); - - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - - /// - /// Downloads the image. - /// - /// The URL. - /// The URL hash. - /// The pointer cache path. - /// Task. - private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) - { - using var result = await _httpClient.GetResponse(new HttpRequestOptions - { - Url = url, - BufferContent = false - - }).ConfigureAwait(false); - var ext = result.ContentType.Split('/').Last(); - - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) - { - using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); - await stream.CopyToAsync(filestream).ConfigureAwait(false); - } - - Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); - File.WriteAllText(pointerCachePath, fullCachePath); - } - - /// - /// Gets the full cache path. - /// - /// The filename. - /// System.String. - private string GetFullCachePath(string filename) - { - return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); - } - } -} From 67efcbee05fe7917aaff11fd27235fb952938434 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Mon, 20 Apr 2020 20:16:58 -0600 Subject: [PATCH 034/724] Remove error handlers, to be implemented at a global level in a separate PR --- .../Controllers/NotificationsController.cs | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index c0c2be626b..2a41f6020e 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -74,14 +74,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public IActionResult GetNotificationTypes() { - try - { - return Ok(_notificationManager.GetNotificationTypes()); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(_notificationManager.GetNotificationTypes()); } /// @@ -92,14 +85,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public IActionResult GetNotificationServices() { - try - { - return Ok(_notificationManager.GetNotificationServices()); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(_notificationManager.GetNotificationServices()); } /// @@ -112,32 +98,23 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult CreateAdminNotification( + public void CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, [FromQuery] string? url, [FromQuery] NotificationLevel? level) { - try + var notification = new NotificationRequest { - var notification = new NotificationRequest - { - Name = name, - Description = description, - Url = url, - Level = level ?? NotificationLevel.Normal, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), - Date = DateTime.UtcNow, - }; + Name = name, + Description = description, + Url = url, + Level = level ?? NotificationLevel.Normal, + UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), + Date = DateTime.UtcNow, + }; - _notificationManager.SendNotification(notification, CancellationToken.None); - - return Ok(); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + _notificationManager.SendNotification(notification, CancellationToken.None); } /// From 6c8e1d37bd49339d298c46c24cddf8e858b334c8 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Mon, 20 Apr 2020 23:53:09 -0600 Subject: [PATCH 035/724] Remove more unnecessary IActionResult --- .../Controllers/NotificationsController.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 2a41f6020e..932b91d55c 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -43,14 +43,14 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IActionResult GetNotifications( + [ProducesResponseType(typeof(NotificationResultDto), StatusCodes.Status200OK)] + public NotificationResultDto GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) { - return Ok(new NotificationResultDto()); + return new NotificationResultDto(); } /// @@ -60,10 +60,10 @@ namespace Jellyfin.Api.Controllers /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)] - public IActionResult GetNotificationsSummary( + public NotificationsSummaryDto GetNotificationsSummary( [FromRoute] string userId) { - return Ok(new NotificationsSummaryDto()); + return new NotificationsSummaryDto(); } /// @@ -71,10 +71,10 @@ namespace Jellyfin.Api.Controllers /// /// All notification types. [HttpGet("Types")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IActionResult GetNotificationTypes() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public IEnumerable GetNotificationTypes() { - return Ok(_notificationManager.GetNotificationTypes()); + return _notificationManager.GetNotificationTypes(); } /// @@ -83,9 +83,9 @@ namespace Jellyfin.Api.Controllers /// All notification services. [HttpGet("Services")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IActionResult GetNotificationServices() + public IEnumerable GetNotificationServices() { - return Ok(_notificationManager.GetNotificationServices()); + return _notificationManager.GetNotificationServices(); } /// From dae69657108f90de54166a670c47a6dff2dae139 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Tue, 21 Apr 2020 00:24:35 -0600 Subject: [PATCH 036/724] Remove documentation of void return type --- Jellyfin.Api/Controllers/NotificationsController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 932b91d55c..c1d9e32515 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -95,7 +95,6 @@ namespace Jellyfin.Api.Controllers /// The description of the notification. /// The URL of the notification. /// The level of the notification. - /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public void CreateAdminNotification( From 1175ce3f97fdebc6fdb489ce65deaac59c7b7f87 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:36:22 -0600 Subject: [PATCH 037/724] Add Exception Middleware --- .../Models/ExceptionDtos/ExceptionDto.cs | 14 +++++ .../ApiApplicationBuilderExtensions.cs | 11 ++++ .../Middleware/ExceptionMiddleware.cs | 60 +++++++++++++++++++ Jellyfin.Server/Startup.cs | 2 + 4 files changed, 87 insertions(+) create mode 100644 Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs create mode 100644 Jellyfin.Server/Middleware/ExceptionMiddleware.cs diff --git a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs new file mode 100644 index 0000000000..d2b48d4ae5 --- /dev/null +++ b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs @@ -0,0 +1,14 @@ +namespace Jellyfin.Api.Models.ExceptionDtos +{ + /// + /// Exception Dto. + /// Used for graceful handling of API exceptions. + /// + public class ExceptionDto + { + /// + /// Gets or sets exception message. + /// + public string Message { get; set; } + } +} diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index db06eb4552..6c105ab65b 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Jellyfin.Server.Middleware; using Microsoft.AspNetCore.Builder; namespace Jellyfin.Server.Extensions @@ -23,5 +24,15 @@ namespace Jellyfin.Server.Extensions c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1"); }); } + + /// + /// Adds exception middleware to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder applicationBuilder) + { + return applicationBuilder.UseMiddleware(); + } } } diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000000..39aace95d2 --- /dev/null +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,60 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Models.ExceptionDtos; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Exception Middleware. + /// + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Next request delegate. + /// Instance of the interface. + public ExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _logger = loggerFactory.CreateLogger() ?? + throw new ArgumentNullException(nameof(loggerFactory)); + } + + /// + /// Invoke request. + /// + /// Request context. + /// Task. + public async Task Invoke(HttpContext context) + { + try + { + await _next(context).ConfigureAwait(false); + } + catch (Exception ex) + { + if (context.Response.HasStarted) + { + _logger.LogWarning("The response has already started, the exception middleware will not be executed."); + throw; + } + + var exceptionBody = new ExceptionDto { Message = ex.Message }; + var exceptionJson = JsonSerializer.Serialize(exceptionBody); + + context.Response.Clear(); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + // TODO switch between PascalCase and camelCase + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(exceptionJson).ConfigureAwait(false); + } + } + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 4d7d56e9d4..7a632f6c44 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -58,6 +58,8 @@ namespace Jellyfin.Server app.UseDeveloperExceptionPage(); } + app.UseExceptionMiddleware(); + app.UseWebSockets(); app.UseResponseCompression(); From 08eba82bb7bebe277f6b106fa48994bb98c3dd41 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:52:33 -0600 Subject: [PATCH 038/724] 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 039/724] 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) { - var deviceInfo = _deviceManager.GetDevice(id); - if (deviceInfo == null) - { - return NotFound(); - } + return NotFound(); + } - return Ok(deviceInfo); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + 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 NotFound(); + } - return Ok(deviceInfo); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + 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) { - var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); - if (existingDeviceOptions == null) - { - return NotFound(); - } + return NotFound(); + } - _deviceManager.UpdateDeviceOptions(id, deviceOptions); - return Ok(); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + _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; + var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; - foreach (var session in sessions) - { - _sessionManager.Logout(session); - } - - 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 04119c0d409342050cb7624f025a21985e10a412 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:55:57 -0600 Subject: [PATCH 040/724] Remove exception handler --- .../DisplayPreferencesController.cs | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0554091b45..e15e9c4be6 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,6 +1,5 @@ #nullable enable -using System; using System.ComponentModel.DataAnnotations; using System.Threading; using MediaBrowser.Controller.Net; @@ -45,20 +44,13 @@ namespace Jellyfin.Api.Controllers [FromQuery] [Required] string userId, [FromQuery] [Required] string client) { - try + var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); + if (result == null) { - var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); - if (result == null) - { - return NotFound(); - } + return NotFound(); + } - return Ok(result); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(result); } /// @@ -80,30 +72,23 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string client, [FromBody, BindRequired] DisplayPreferences displayPreferences) { - try + if (!ModelState.IsValid) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (displayPreferencesId == null) - { - // do nothing. - } - - _displayPreferencesRepository.SaveDisplayPreferences( - displayPreferences, - userId, - client, - CancellationToken.None); - - return Ok(); + return BadRequest(ModelState); } - catch (Exception e) + + if (displayPreferencesId == null) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + // do nothing. } + + _displayPreferencesRepository.SaveDisplayPreferences( + displayPreferences, + userId, + client, + CancellationToken.None); + + return Ok(); } } } From 30609236ab58532d021e45edcdacd32d78aeca94 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:57:45 -0600 Subject: [PATCH 041/724] Remove exception handler --- .../Images/ImageByNameController.cs | 78 +++++-------------- 1 file changed, 18 insertions(+), 60 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index 3097296051..4034c9e857 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -48,14 +48,7 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetGeneralImages() { - try - { - return Ok(GetImageList(_applicationPaths.GeneralPath, false)); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(GetImageList(_applicationPaths.GeneralPath, false)); } /// @@ -70,28 +63,21 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type) { - try + var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) + ? "folder" + : type; + + var paths = BaseItem.SupportedImageExtensions + .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList(); + + var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault(); + if (path == null || !System.IO.File.Exists(path)) { - var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) - ? "folder" - : type; - - var paths = BaseItem.SupportedImageExtensions - .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList(); - - var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault(); - if (path == null || !System.IO.File.Exists(path)) - { - return NotFound(); - } - - var contentType = MimeTypes.GetMimeType(path); - return new FileStreamResult(System.IO.File.OpenRead(path), contentType); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + var contentType = MimeTypes.GetMimeType(path); + return new FileStreamResult(System.IO.File.OpenRead(path), contentType); } /// @@ -103,14 +89,7 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetRatingImages() { - try - { - return Ok(GetImageList(_applicationPaths.RatingsPath, false)); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(GetImageList(_applicationPaths.RatingsPath, false)); } /// @@ -127,14 +106,7 @@ namespace Jellyfin.Api.Controllers.Images [FromRoute] string theme, [FromRoute] string name) { - try - { - return GetImageFile(_applicationPaths.RatingsPath, theme, name); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return GetImageFile(_applicationPaths.RatingsPath, theme, name); } /// @@ -146,14 +118,7 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetMediaInfoImages() { - try - { - return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false)); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false)); } /// @@ -170,14 +135,7 @@ namespace Jellyfin.Api.Controllers.Images [FromRoute] string theme, [FromRoute] string name) { - try - { - return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); } /// From a6cd8526758386045a6895b0037f2199bdcb9003 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:58:54 -0600 Subject: [PATCH 042/724] Remove exception handler --- .../Images/RemoteImageController.cs | 196 ++++++++---------- 1 file changed, 84 insertions(+), 112 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs index 66479582da..8c7d21cd53 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs @@ -74,57 +74,50 @@ namespace Jellyfin.Api.Controllers.Images [FromQuery] string providerName, [FromQuery] bool includeAllLanguages) { - try + var item = _libraryManager.GetItemById(id); + if (item == null) { - var item = _libraryManager.GetItemById(id); - if (item == null) - { - return NotFound(); - } - - var images = await _providerManager.GetAvailableRemoteImages( - item, - new RemoteImageQuery - { - ProviderName = providerName, - IncludeAllLanguages = includeAllLanguages, - IncludeDisabledProviders = true, - ImageType = type - }, CancellationToken.None) - .ConfigureAwait(false); - - var imageArray = images.ToArray(); - var allProviders = _providerManager.GetRemoteImageProviderInfo(item); - if (type.HasValue) - { - allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value)); - } - - var result = new RemoteImageResult - { - TotalRecordCount = imageArray.Length, - Providers = allProviders.Select(o => o.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() - }; - - if (startIndex.HasValue) - { - imageArray = imageArray.Skip(startIndex.Value).ToArray(); - } - - if (limit.HasValue) - { - imageArray = imageArray.Take(limit.Value).ToArray(); - } - - result.Images = imageArray; - return Ok(result); + return NotFound(); } - catch (Exception e) + + var images = await _providerManager.GetAvailableRemoteImages( + item, + new RemoteImageQuery + { + ProviderName = providerName, + IncludeAllLanguages = includeAllLanguages, + IncludeDisabledProviders = true, + ImageType = type + }, CancellationToken.None) + .ConfigureAwait(false); + + var imageArray = images.ToArray(); + var allProviders = _providerManager.GetRemoteImageProviderInfo(item); + if (type.HasValue) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value)); } + + var result = new RemoteImageResult + { + TotalRecordCount = imageArray.Length, + Providers = allProviders.Select(o => o.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() + }; + + if (startIndex.HasValue) + { + imageArray = imageArray.Skip(startIndex.Value).ToArray(); + } + + if (limit.HasValue) + { + imageArray = imageArray.Take(limit.Value).ToArray(); + } + + result.Images = imageArray; + return Ok(result); } /// @@ -138,21 +131,14 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetRemoteImageProviders([FromRoute] string id) { - try + var item = _libraryManager.GetItemById(id); + if (item == null) { - var item = _libraryManager.GetItemById(id); - if (item == null) - { - return NotFound(); - } + return NotFound(); + } - var providers = _providerManager.GetRemoteImageProviderInfo(item); - return Ok(providers); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + var providers = _providerManager.GetRemoteImageProviderInfo(item); + return Ok(providers); } /// @@ -166,49 +152,42 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public async Task GetRemoteImage([FromQuery, BindRequired] string imageUrl) { + var urlHash = imageUrl.GetMD5(); + var pointerCachePath = GetFullCachePath(urlHash.ToString()); + + string? contentPath = null; + bool hasFile = false; + try { - var urlHash = imageUrl.GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string? contentPath = null; - bool hasFile = false; - - try + contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); + if (System.IO.File.Exists(contentPath)) { - contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - if (System.IO.File.Exists(contentPath)) - { - hasFile = true; - } + hasFile = true; } - catch (FileNotFoundException) - { - // Means the file isn't cached yet - } - catch (IOException) - { - // Means the file isn't cached yet - } - - if (!hasFile) - { - await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - } - - if (string.IsNullOrEmpty(contentPath)) - { - return NotFound(); - } - - var contentType = MimeTypes.GetMimeType(contentPath); - return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType); } - catch (Exception e) + catch (FileNotFoundException) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + // Means the file isn't cached yet } + catch (IOException) + { + // Means the file isn't cached yet + } + + if (!hasFile) + { + await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); + contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); + } + + if (string.IsNullOrEmpty(contentPath)) + { + return NotFound(); + } + + var contentType = MimeTypes.GetMimeType(contentPath); + return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType); } /// @@ -227,24 +206,17 @@ namespace Jellyfin.Api.Controllers.Images [FromQuery, BindRequired] ImageType type, [FromQuery] string imageUrl) { - try + var item = _libraryManager.GetItemById(id); + if (item == null) { - var item = _libraryManager.GetItemById(id); - if (item == null) - { - return NotFound(); - } - - await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) - .ConfigureAwait(false); - - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); - return Ok(); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) + .ConfigureAwait(false); + + item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + return Ok(); } /// From 8ab9949db5a1c0072ec35937cb96e93ce5b9d672 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:02:07 -0600 Subject: [PATCH 043/724] Remove exception handler --- .../Controllers/ScheduledTasksController.cs | 151 +++++++----------- 1 file changed, 59 insertions(+), 92 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 157e985197..acbc630c23 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -41,48 +41,41 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isHidden = false, [FromQuery] bool? isEnabled = false) { - try + IEnumerable tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); + + if (isHidden.HasValue) { - IEnumerable tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); - - if (isHidden.HasValue) + var hiddenValue = isHidden.Value; + tasks = tasks.Where(o => { - var hiddenValue = isHidden.Value; - tasks = tasks.Where(o => + var itemIsHidden = false; + if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) { - var itemIsHidden = false; - if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) - { - itemIsHidden = configurableScheduledTask.IsHidden; - } + itemIsHidden = configurableScheduledTask.IsHidden; + } - return itemIsHidden == hiddenValue; - }); - } - - if (isEnabled.HasValue) - { - var enabledValue = isEnabled.Value; - tasks = tasks.Where(o => - { - var itemIsEnabled = false; - if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) - { - itemIsEnabled = configurableScheduledTask.IsEnabled; - } - - return itemIsEnabled == enabledValue; - }); - } - - var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo); - - return Ok(taskInfos); + return itemIsHidden == hiddenValue; + }); } - catch (Exception e) + + if (isEnabled.HasValue) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + var enabledValue = isEnabled.Value; + tasks = tasks.Where(o => + { + var itemIsEnabled = false; + if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) + { + itemIsEnabled = configurableScheduledTask.IsEnabled; + } + + return itemIsEnabled == enabledValue; + }); } + + var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo); + + return Ok(taskInfos); } /// @@ -96,23 +89,16 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetTask([FromRoute] string taskId) { - try - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(i => - string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); + var task = _taskManager.ScheduledTasks.FirstOrDefault(i => + string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); - if (task == null) - { - return NotFound(); - } - - var result = ScheduledTaskHelpers.GetTaskInfo(task); - return Ok(result); - } - catch (Exception e) + if (task == null) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + var result = ScheduledTaskHelpers.GetTaskInfo(task); + return Ok(result); } /// @@ -126,23 +112,16 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult StartTask([FromRoute] string taskId) { - try - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(o => - o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); - if (task == null) - { - return NotFound(); - } - - _taskManager.Execute(task, new TaskOptions()); - return Ok(); - } - catch (Exception e) + if (task == null) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + _taskManager.Execute(task, new TaskOptions()); + return Ok(); } /// @@ -156,23 +135,16 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult StopTask([FromRoute] string taskId) { - try - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(o => - o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); - if (task == null) - { - return NotFound(); - } - - _taskManager.Cancel(task); - return Ok(); - } - catch (Exception e) + if (task == null) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + _taskManager.Cancel(task); + return Ok(); } /// @@ -185,24 +157,19 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult UpdateTask([FromRoute] string taskId, [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos) + public IActionResult UpdateTask( + [FromRoute] string taskId, + [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos) { - try + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + if (task == null) { - var task = _taskManager.ScheduledTasks.FirstOrDefault(o => - o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); - if (task == null) - { - return NotFound(); - } + return NotFound(); + } - task.Triggers = triggerInfos; - return Ok(); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + task.Triggers = triggerInfos; + return Ok(); } } } From fe632146dcba69edeec56b850736227ff5f4c5b3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:17:13 -0600 Subject: [PATCH 044/724] Move Json Options to static class for easier access. --- .../CamelCaseJsonProfileFormatter.cs | 4 +- .../PascalCaseJsonProfileFormatter.cs | 4 +- Jellyfin.Server/Models/JsonOptions.cs | 41 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 Jellyfin.Server/Models/JsonOptions.cs diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index 433a3197d3..e6ad6dfb13 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using Jellyfin.Server.Models; 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(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) + public CamelCaseJsonProfileFormatter() : base(JsonOptions.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 2ed006a336..675f4c79ee 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using Jellyfin.Server.Models; 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(new JsonSerializerOptions { PropertyNamingPolicy = null }) + public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase) { SupportedMediaTypes.Clear(); // Add application/json for default formatter diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs new file mode 100644 index 0000000000..fa503bc9a4 --- /dev/null +++ b/Jellyfin.Server/Models/JsonOptions.cs @@ -0,0 +1,41 @@ +using System.Text.Json; + +namespace Jellyfin.Server.Models +{ + /// + /// Json Options. + /// + public static class JsonOptions + { + /// + /// Base Json Serializer Options. + /// + private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions(); + + /// + /// Gets CamelCase json options. + /// + public static JsonSerializerOptions CamelCase + { + get + { + var options = _jsonOptions; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + return options; + } + } + + /// + /// Gets PascalCase json options. + /// + public static JsonSerializerOptions PascalCase + { + get + { + var options = _jsonOptions; + options.PropertyNamingPolicy = null; + return options; + } + } + } +} From 14361c68cf71bc810d282901a764d2f8d5858eea Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:38:31 -0600 Subject: [PATCH 045/724] Add ProducesResponseType to base controller --- Jellyfin.Api/BaseJellyfinApiController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 1f4508e6cb..f691759866 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,3 +1,5 @@ +using Jellyfin.Api.Models.ExceptionDtos; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api @@ -7,6 +9,7 @@ namespace Jellyfin.Api /// [ApiController] [Route("[controller]")] + [ProducesResponseType(typeof(ExceptionDto), StatusCodes.Status500InternalServerError)] public class BaseJellyfinApiController : ControllerBase { } From b8fd9c785e107b6d2ae8125d6e6b6374f36fe9a3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:42:48 -0600 Subject: [PATCH 046/724] Convert StartupController to IActionResult --- Jellyfin.Api/Controllers/StartupController.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index afc9b8f3da..b0b26c1762 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Models.StartupDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers @@ -32,12 +33,15 @@ namespace Jellyfin.Api.Controllers /// /// Api endpoint for completing the startup wizard. /// + /// Status. [HttpPost("Complete")] - public void CompleteWizard() + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); _config.SaveConfiguration(); + return Ok(); } /// @@ -45,7 +49,8 @@ namespace Jellyfin.Api.Controllers /// /// The initial startup wizard configuration. [HttpGet("Configuration")] - public StartupConfigurationDto GetStartupConfiguration() + [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)] + public IActionResult GetStartupConfiguration() { var result = new StartupConfigurationDto { @@ -54,7 +59,7 @@ namespace Jellyfin.Api.Controllers PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage }; - return result; + return Ok(result); } /// @@ -63,8 +68,10 @@ namespace Jellyfin.Api.Controllers /// The UI language culture. /// The metadata country code. /// The preferred language for metadata. + /// Status. [HttpPost("Configuration")] - public void UpdateInitialConfiguration( + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, [FromForm] string preferredMetadataLanguage) @@ -73,6 +80,7 @@ namespace Jellyfin.Api.Controllers _config.Configuration.MetadataCountryCode = metadataCountryCode; _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; _config.SaveConfiguration(); + return Ok(); } /// @@ -80,12 +88,15 @@ namespace Jellyfin.Api.Controllers /// /// Enable remote access. /// Enable UPnP. + /// Status. [HttpPost("RemoteAccess")] - public void SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; _config.SaveConfiguration(); + return Ok(); } /// @@ -93,14 +104,11 @@ namespace Jellyfin.Api.Controllers /// /// The first user. [HttpGet("User")] - public StartupUserDto GetFirstUser() + [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)] + public IActionResult GetFirstUser() { var user = _userManager.Users.First(); - return new StartupUserDto - { - Name = user.Name, - Password = user.Password - }; + return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); } /// @@ -109,7 +117,8 @@ namespace Jellyfin.Api.Controllers /// The DTO containing username and password. /// The async task. [HttpPost("User")] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); @@ -121,6 +130,8 @@ namespace Jellyfin.Api.Controllers { await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); } + + return Ok(); } } } From 3ef8448a518e673feae0c70c2682d60e4632c0cd Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 09:09:05 -0600 Subject: [PATCH 047/724] Return to previous exception handle implementation --- Jellyfin.Api/BaseJellyfinApiController.cs | 3 - .../Models/ExceptionDtos/ExceptionDto.cs | 14 --- .../Middleware/ExceptionMiddleware.cs | 86 ++++++++++++++++--- 3 files changed, 73 insertions(+), 30 deletions(-) delete mode 100644 Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index f691759866..1f4508e6cb 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,5 +1,3 @@ -using Jellyfin.Api.Models.ExceptionDtos; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api @@ -9,7 +7,6 @@ namespace Jellyfin.Api /// [ApiController] [Route("[controller]")] - [ProducesResponseType(typeof(ExceptionDto), StatusCodes.Status500InternalServerError)] public class BaseJellyfinApiController : ControllerBase { } diff --git a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs deleted file mode 100644 index d2b48d4ae5..0000000000 --- a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Jellyfin.Api.Models.ExceptionDtos -{ - /// - /// Exception Dto. - /// Used for graceful handling of API exceptions. - /// - public class ExceptionDto - { - /// - /// Gets or sets exception message. - /// - public string Message { get; set; } - } -} diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 39aace95d2..0d9dac89f0 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -1,7 +1,9 @@ using System; -using System.Text.Json; +using System.IO; using System.Threading.Tasks; -using Jellyfin.Api.Models.ExceptionDtos; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -14,17 +16,22 @@ namespace Jellyfin.Server.Middleware { private readonly RequestDelegate _next; private readonly ILogger _logger; + private readonly IServerConfigurationManager _configuration; /// /// Initializes a new instance of the class. /// /// Next request delegate. /// Instance of the interface. - public ExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) + /// Instance of the interface. + public ExceptionMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IServerConfigurationManager serverConfigurationManager) { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _logger = loggerFactory.CreateLogger() ?? - throw new ArgumentNullException(nameof(loggerFactory)); + _next = next; + _logger = loggerFactory.CreateLogger(); + _configuration = serverConfigurationManager; } /// @@ -46,15 +53,68 @@ namespace Jellyfin.Server.Middleware throw; } - var exceptionBody = new ExceptionDto { Message = ex.Message }; - var exceptionJson = JsonSerializer.Serialize(exceptionBody); + ex = GetActualException(ex); + _logger.LogError(ex, "Error processing request: {0}", ex.Message); + context.Response.StatusCode = GetStatusCode(ex); + context.Response.ContentType = "text/plain"; - context.Response.Clear(); - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - // TODO switch between PascalCase and camelCase - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync(exceptionJson).ConfigureAwait(false); + var errorContent = NormalizeExceptionMessage(ex.Message); + await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } } + + private static Exception GetActualException(Exception ex) + { + if (ex is AggregateException agg) + { + var inner = agg.InnerException; + if (inner != null) + { + return GetActualException(inner); + } + + var inners = agg.InnerExceptions; + if (inners.Count > 0) + { + return GetActualException(inners[0]); + } + } + + return ex; + } + + private static int GetStatusCode(Exception ex) + { + switch (ex) + { + case ArgumentException _: return StatusCodes.Status400BadRequest; + case SecurityException _: return StatusCodes.Status401Unauthorized; + case DirectoryNotFoundException _: + case FileNotFoundException _: + case ResourceNotFoundException _: return StatusCodes.Status404NotFound; + case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; + default: return StatusCodes.Status500InternalServerError; + } + } + + private string NormalizeExceptionMessage(string msg) + { + if (msg == null) + { + return string.Empty; + } + + // Strip any information we don't want to reveal + msg = msg.Replace( + _configuration.ApplicationPaths.ProgramSystemPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); + msg = msg.Replace( + _configuration.ApplicationPaths.ProgramDataPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); + + return msg; + } } } From 69d9bfb233bd2716e3803b38c55275de58bb8d46 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Tue, 21 Apr 2020 12:10:34 -0600 Subject: [PATCH 048/724] Make documentation of parameters clearer Co-Authored-By: Vasily --- Jellyfin.Api/Controllers/NotificationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index c1d9e32515..6145246ed3 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -38,7 +38,7 @@ namespace Jellyfin.Api.Controllers /// Endpoint for getting a user's notifications. /// /// The user's ID. - /// An optional filter by IsRead. + /// An optional filter by notification read state. /// The optional index to start at. All notifications with a lower index will be dropped from the results. /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. From 466e20ea8cb8c262605d06dc01eff4463559d9b0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 13:57:11 -0600 Subject: [PATCH 049/724] 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 050/724] 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 98224dee9e3bfc2bb30c14792aec4bda47670863 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:01:47 -0600 Subject: [PATCH 051/724] move to ActionResult --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index e15e9c4be6..25391bcf84 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -36,10 +36,9 @@ namespace Jellyfin.Api.Controllers /// Client. /// Display Preferences. [HttpGet("{DisplayPreferencesId}")] - [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetDisplayPreferences( + public ActionResult GetDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery] [Required] string userId, [FromQuery] [Required] string client) @@ -65,8 +64,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult UpdateDisplayPreferences( + public ActionResult UpdateDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery, BindRequired] string userId, [FromQuery, BindRequired] string client, From 02a78aaae98bdecacd04325e124bde9224c66955 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:07:11 -0600 Subject: [PATCH 052/724] move to ActionResult --- .../Images/ImageByNameController.cs | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index 4034c9e857..ce509b4e6d 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -44,9 +44,8 @@ namespace Jellyfin.Api.Controllers.Images /// /// General images. [HttpGet("General")] - [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetGeneralImages() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetGeneralImages() { return Ok(GetImageList(_applicationPaths.GeneralPath, false)); } @@ -58,10 +57,10 @@ namespace Jellyfin.Api.Controllers.Images /// Image Type (primary, backdrop, logo, etc). /// Image Stream. [HttpGet("General/{Name}/{Type}")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [Produces("application/octet-stream")] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type) + public ActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type) { var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) ? "folder" @@ -85,9 +84,8 @@ namespace Jellyfin.Api.Controllers.Images /// /// General images. [HttpGet("Ratings")] - [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetRatingImages() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetRatingImages() { return Ok(GetImageList(_applicationPaths.RatingsPath, false)); } @@ -99,10 +97,10 @@ namespace Jellyfin.Api.Controllers.Images /// The name of the image. /// Image Stream. [HttpGet("Ratings/{Theme}/{Name}")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [Produces("application/octet-stream")] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetRatingImage( + public ActionResult GetRatingImage( [FromRoute] string theme, [FromRoute] string name) { @@ -114,9 +112,8 @@ namespace Jellyfin.Api.Controllers.Images /// /// Media Info images. [HttpGet("MediaInfo")] - [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetMediaInfoImages() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetMediaInfoImages() { return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false)); } @@ -128,10 +125,10 @@ namespace Jellyfin.Api.Controllers.Images /// The name of the image. /// Image Stream. [HttpGet("MediaInfo/{Theme}/{Name}")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [Produces("application/octet-stream")] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetMediaInfoImage( + public ActionResult GetMediaInfoImage( [FromRoute] string theme, [FromRoute] string name) { @@ -145,7 +142,7 @@ namespace Jellyfin.Api.Controllers.Images /// Theme to search. /// File name to search for. /// Image Stream. - private IActionResult GetImageFile(string basePath, string theme, string name) + private ActionResult GetImageFile(string basePath, string theme, string name) { var themeFolder = Path.Combine(basePath, theme); if (Directory.Exists(themeFolder)) From 9ae895ba213a508f676d21e5425b25bb518ed89a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:09:06 -0600 Subject: [PATCH 053/724] move to ActionResult --- .../Images/RemoteImageController.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs index 8c7d21cd53..a0754ed4eb 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs @@ -63,10 +63,9 @@ namespace Jellyfin.Api.Controllers.Images /// Optinal. Include all languages. /// Remote Image Result. [HttpGet("{Id}/RemoteImages")] - [ProducesResponseType(typeof(RemoteImageResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)] - public async Task GetRemoteImages( + public async Task> GetRemoteImages( [FromRoute] string id, [FromQuery] ImageType? type, [FromQuery] int? startIndex, @@ -126,10 +125,9 @@ namespace Jellyfin.Api.Controllers.Images /// Item Id. /// List of providers. [HttpGet("{Id}/RemoteImages/Providers")] - [ProducesResponseType(typeof(ImageProviderInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetRemoteImageProviders([FromRoute] string id) + public ActionResult GetRemoteImageProviders([FromRoute] string id) { var item = _libraryManager.GetItemById(id); if (item == null) @@ -147,10 +145,10 @@ namespace Jellyfin.Api.Controllers.Images /// The image url. /// Image Stream. [HttpGet("Remote")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [Produces("application/octet-stream")] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public async Task GetRemoteImage([FromQuery, BindRequired] string imageUrl) + public async Task> GetRemoteImage([FromQuery, BindRequired] string imageUrl) { var urlHash = imageUrl.GetMD5(); var pointerCachePath = GetFullCachePath(urlHash.ToString()); @@ -200,8 +198,7 @@ namespace Jellyfin.Api.Controllers.Images [HttpPost("{Id}/RemoteImages/Download")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public async Task DownloadRemoteImage( + public async Task DownloadRemoteImage( [FromRoute] string id, [FromQuery, BindRequired] ImageType type, [FromQuery] string imageUrl) From 88b856796a9e4852ae4f9938baddd4741e8285d5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:23:08 -0600 Subject: [PATCH 054/724] move to ActionResult --- .../Controllers/ScheduledTasksController.cs | 53 ++++++------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index acbc630c23..da7cfbc3a7 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Api.Controllers /// /// Scheduled Tasks Controller. /// - [Authenticated] + // [Authenticated] public class ScheduledTasksController : BaseJellyfinApiController { private readonly ITaskManager _taskManager; @@ -35,47 +35,30 @@ namespace Jellyfin.Api.Controllers /// Optional filter tasks that are enabled, or not. /// Task list. [HttpGet] - [ProducesResponseType(typeof(TaskInfo[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetTasks( + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetTasks( [FromQuery] bool? isHidden = false, [FromQuery] bool? isEnabled = false) { IEnumerable tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); - if (isHidden.HasValue) + foreach (var task in tasks) { - var hiddenValue = isHidden.Value; - tasks = tasks.Where(o => + if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask) { - var itemIsHidden = false; - if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) + if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden) { - itemIsHidden = configurableScheduledTask.IsHidden; + continue; } - return itemIsHidden == hiddenValue; - }); - } - - if (isEnabled.HasValue) - { - var enabledValue = isEnabled.Value; - tasks = tasks.Where(o => - { - var itemIsEnabled = false; - if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask) + if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled) { - itemIsEnabled = configurableScheduledTask.IsEnabled; + continue; } + } - return itemIsEnabled == enabledValue; - }); + yield return task; } - - var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo); - - return Ok(taskInfos); } /// @@ -84,10 +67,9 @@ namespace Jellyfin.Api.Controllers /// Task Id. /// Task Info. [HttpGet("{TaskID}")] - [ProducesResponseType(typeof(TaskInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetTask([FromRoute] string taskId) + public ActionResult GetTask([FromRoute] string taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); @@ -109,8 +91,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Running/{TaskID}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult StartTask([FromRoute] string taskId) + public ActionResult StartTask([FromRoute] string taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); @@ -132,8 +113,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Running/{TaskID}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult StopTask([FromRoute] string taskId) + public ActionResult StopTask([FromRoute] string taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); @@ -156,8 +136,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("{TaskID}/Triggers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult UpdateTask( + public ActionResult UpdateTask( [FromRoute] string taskId, [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos) { From 7db3b035a6d1f7e6f4886c4497b98b7a6af6c679 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:25:03 -0600 Subject: [PATCH 055/724] move to ActionResult --- Jellyfin.Api/Controllers/StartupController.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index b0b26c1762..2db7e32aad 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult CompleteWizard() + public ActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); @@ -49,8 +49,8 @@ namespace Jellyfin.Api.Controllers /// /// The initial startup wizard configuration. [HttpGet("Configuration")] - [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)] - public IActionResult GetStartupConfiguration() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStartupConfiguration() { var result = new StartupConfigurationDto { @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult UpdateInitialConfiguration( + public ActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, [FromForm] string preferredMetadataLanguage) @@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; @@ -104,8 +104,8 @@ namespace Jellyfin.Api.Controllers /// /// The first user. [HttpGet("User")] - [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)] - public IActionResult GetFirstUser() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetFirstUser() { var user = _userManager.Users.First(); return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); @@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers /// The async task. [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); From 3ab61dbdc252670abf28797d3183614b1cd05ece Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 15:49:04 -0600 Subject: [PATCH 056/724] bump swashbuckle --- Jellyfin.Api/Jellyfin.Api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index cbb1d3007f..77bb52c6a5 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -10,8 +10,8 @@ - - + + From 2542a27bd5f79ccfbc2547ddd877ddb0423ae296 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 16:15:31 -0600 Subject: [PATCH 057/724] Fix documentation endpoint for use with baseUrl --- .../ApiApplicationBuilderExtensions.cs | 28 ++++++++++++++----- Jellyfin.Server/Startup.cs | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 43c49307d4..df3bab931b 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; namespace Jellyfin.Server.Extensions @@ -11,23 +12,36 @@ namespace Jellyfin.Server.Extensions /// Adds swagger and swagger UI to the application pipeline. /// /// The application builder. + /// The server configuration. /// The updated application builder. - public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder) + public static IApplicationBuilder UseJellyfinApiSwagger( + this IApplicationBuilder applicationBuilder, + IServerConfigurationManager serverConfigurationManager) { // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. - const string specEndpoint = "/swagger/v1/swagger.json"; + + var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/'); + if (!string.IsNullOrEmpty(baseUrl)) + { + baseUrl += '/'; + } + return applicationBuilder - .UseSwagger() + .UseSwagger(c => + { + c.RouteTemplate = $"/{baseUrl}api-docs/{{documentName}}/openapi.json"; + }) .UseSwaggerUI(c => { - c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); - c.RoutePrefix = "api-docs/swagger"; + c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1"); + c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger"; }) .UseReDoc(c => { - c.SpecUrl(specEndpoint); - c.RoutePrefix = "api-docs/redoc"; + c.DocumentTitle = "Jellyfin API v1"; + c.SpecUrl($"/{baseUrl}api-docs/v1/openapi.json"); + c.RoutePrefix = $"{baseUrl}api-docs/v1/redoc"; }); } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 4d7d56e9d4..ee08d2580a 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -66,7 +66,7 @@ namespace Jellyfin.Server app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync); // TODO use when old API is removed: app.UseAuthentication(); - app.UseJellyfinApiSwagger(); + app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => From 041d674eb6e4a675b68406ed5c2d7018d61e870a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 16:19:26 -0600 Subject: [PATCH 058/724] Fix swagger ui Document Title --- Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index df3bab931b..d094242259 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -34,6 +34,7 @@ namespace Jellyfin.Server.Extensions }) .UseSwaggerUI(c => { + c.DocumentTitle = "Jellyfin API v1"; c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1"); c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger"; }) From f5385e4735849cbb1552e69faa0116e5498b3688 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 18:12:46 -0600 Subject: [PATCH 059/724] Move Emby.Dlna DlnaService.cs to Jellyfin.Api --- Emby.Dlna/Api/DlnaService.cs | 88 --------------- Jellyfin.Api/Controllers/DlnaController.cs | 124 +++++++++++++++++++++ 2 files changed, 124 insertions(+), 88 deletions(-) delete mode 100644 Emby.Dlna/Api/DlnaService.cs create mode 100644 Jellyfin.Api/Controllers/DlnaController.cs diff --git a/Emby.Dlna/Api/DlnaService.cs b/Emby.Dlna/Api/DlnaService.cs deleted file mode 100644 index 5f984bb335..0000000000 --- a/Emby.Dlna/Api/DlnaService.cs +++ /dev/null @@ -1,88 +0,0 @@ -#pragma warning disable CS1591 - -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Services; - -namespace Emby.Dlna.Api -{ - [Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")] - public class GetProfileInfos : IReturn - { - } - - [Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")] - public class DeleteProfile : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - [Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")] - public class GetDefaultProfile : IReturn - { - } - - [Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")] - public class GetProfile : IReturn - { - [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")] - public class UpdateProfile : DeviceProfile, IReturnVoid - { - } - - [Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")] - public class CreateProfile : DeviceProfile, IReturnVoid - { - } - - [Authenticated(Roles = "Admin")] - public class DlnaService : IService - { - private readonly IDlnaManager _dlnaManager; - - public DlnaService(IDlnaManager dlnaManager) - { - _dlnaManager = dlnaManager; - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetProfileInfos request) - { - return _dlnaManager.GetProfileInfos().ToArray(); - } - - public object Get(GetProfile request) - { - return _dlnaManager.GetProfile(request.Id); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetDefaultProfile request) - { - return _dlnaManager.GetDefaultProfile(); - } - - public void Delete(DeleteProfile request) - { - _dlnaManager.DeleteProfile(request.Id); - } - - public void Post(UpdateProfile request) - { - _dlnaManager.UpdateProfile(request); - } - - public void Post(CreateProfile request) - { - _dlnaManager.CreateProfile(request); - } - } -} diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs new file mode 100644 index 0000000000..68cd144f4e --- /dev/null +++ b/Jellyfin.Api/Controllers/DlnaController.cs @@ -0,0 +1,124 @@ +#nullable enable + +using System.Collections.Generic; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Dlna Controller. + /// + [Authenticated(Roles = "Admin")] + public class DlnaController : BaseJellyfinApiController + { + private readonly IDlnaManager _dlnaManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public DlnaController(IDlnaManager dlnaManager) + { + _dlnaManager = dlnaManager; + } + + /// + /// Get profile infos. + /// + /// Profile infos. + [HttpGet("ProfileInfos")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetProfileInfos() + { + return _dlnaManager.GetProfileInfos(); + } + + /// + /// Gets the default profile. + /// + /// Default profile. + [HttpGet("Profiles/Default")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultProfile() + { + return Ok(_dlnaManager.GetDefaultProfile()); + } + + /// + /// Gets a single profile. + /// + /// Profile Id. + /// Profile. + [HttpGet("Profiles/{Id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetProfile([FromRoute] string id) + { + var profile = _dlnaManager.GetProfile(id); + if (profile == null) + { + return NotFound(); + } + + return Ok(profile); + } + + /// + /// Deletes a profile. + /// + /// Profile id. + /// Status. + [HttpDelete("Profiles/{Id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteProfile([FromRoute] string id) + { + var existingDeviceProfile = _dlnaManager.GetProfile(id); + if (existingDeviceProfile == null) + { + return NotFound(); + } + + _dlnaManager.DeleteProfile(id); + return Ok(); + } + + /// + /// Creates a profile. + /// + /// Device profile. + /// Status. + [HttpPost("Profiles")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile) + { + _dlnaManager.CreateProfile(deviceProfile); + return Ok(); + } + + /// + /// Updates a profile. + /// + /// Profile id. + /// Device profile. + /// Status. + [HttpPost("Profiles/{Id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UpdateProfile([FromRoute] string id, [FromBody] DeviceProfile deviceProfile) + { + var existingDeviceProfile = _dlnaManager.GetProfile(id); + if (existingDeviceProfile == null) + { + return NotFound(); + } + + _dlnaManager.UpdateProfile(deviceProfile); + return Ok(); + } + } +} From 461b298be7247afd7f7962604efab3b58b9dae4b Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 19:15:27 -0600 Subject: [PATCH 060/724] Migrate DlnaServerController to Jellyfin.Api --- Emby.Dlna/Api/DlnaServerService.cs | 383 ------------------ Emby.Dlna/Main/DlnaEntryPoint.cs | 6 +- .../Attributes/HttpSubscribeAttribute.cs | 35 ++ .../Attributes/HttpUnsubscribeAttribute.cs | 35 ++ .../Controllers/DlnaServerController.cs | 259 ++++++++++++ Jellyfin.Api/Jellyfin.Api.csproj | 1 + 6 files changed, 333 insertions(+), 386 deletions(-) delete mode 100644 Emby.Dlna/Api/DlnaServerService.cs create mode 100644 Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs create mode 100644 Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs create mode 100644 Jellyfin.Api/Controllers/DlnaServerController.cs diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs deleted file mode 100644 index 7fba2184a7..0000000000 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ /dev/null @@ -1,383 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using Emby.Dlna.Main; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; - -namespace Emby.Dlna.Api -{ - [Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")] - [Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")] - public class GetDescriptionXml - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")] - [Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")] - public class GetContentDirectory - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")] - [Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")] - public class GetConnnectionManager - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")] - [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")] - public class GetMediaReceiverRegistrar - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")] - public class ProcessContentDirectoryControlRequest : IRequiresRequestStream - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")] - public class ProcessConnectionManagerControlRequest : IRequiresRequestStream - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")] - public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UuId { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")] - [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")] - public class ProcessMediaReceiverRegistrarEventRequest - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")] - [Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")] - public class ProcessContentDirectoryEventRequest - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")] - [Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")] - public class ProcessConnectionManagerEventRequest - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")] - public string UuId { get; set; } - } - - [Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")] - [Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")] - public class GetIcon - { - [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string UuId { get; set; } - - [ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Filename { get; set; } - } - - public class DlnaServerService : IService, IRequiresRequest - { - private const string XMLContentType = "text/xml; charset=UTF-8"; - - private readonly IDlnaManager _dlnaManager; - private readonly IHttpResultFactory _resultFactory; - private readonly IServerConfigurationManager _configurationManager; - - public IRequest Request { get; set; } - - private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory; - - private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager; - - private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar; - - public DlnaServerService( - IDlnaManager dlnaManager, - IHttpResultFactory httpResultFactory, - IServerConfigurationManager configurationManager) - { - _dlnaManager = dlnaManager; - _resultFactory = httpResultFactory; - _configurationManager = configurationManager; - } - - private string GetHeader(string name) - { - return Request.Headers[name]; - } - - public object Get(GetDescriptionXml request) - { - var url = Request.AbsoluteUri; - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress); - - var cacheLength = TimeSpan.FromDays(1); - var cacheKey = Request.RawUrl.GetMD5(); - var bytes = Encoding.UTF8.GetBytes(xml); - - return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult(new MemoryStream(bytes))); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetContentDirectory request) - { - var xml = ContentDirectory.GetServiceXml(); - - return _resultFactory.GetResult(Request, xml, XMLContentType); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetMediaReceiverRegistrar request) - { - var xml = MediaReceiverRegistrar.GetServiceXml(); - - return _resultFactory.GetResult(Request, xml, XMLContentType); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetConnnectionManager request) - { - var xml = ConnectionManager.GetServiceXml(); - - return _resultFactory.GetResult(Request, xml, XMLContentType); - } - - public async Task Post(ProcessMediaReceiverRegistrarControlRequest request) - { - var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false); - - return _resultFactory.GetResult(Request, response.Xml, XMLContentType); - } - - public async Task Post(ProcessContentDirectoryControlRequest request) - { - var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false); - - return _resultFactory.GetResult(Request, response.Xml, XMLContentType); - } - - public async Task Post(ProcessConnectionManagerControlRequest request) - { - var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false); - - return _resultFactory.GetResult(Request, response.Xml, XMLContentType); - } - - private Task PostAsync(Stream requestStream, IUpnpService service) - { - var id = GetPathValue(2).ToString(); - - return service.ProcessControlRequestAsync(new ControlRequest - { - Headers = Request.Headers, - InputXml = requestStream, - TargetServerUuId = id, - RequestedUrl = Request.AbsoluteUri - }); - } - - // Copied from MediaBrowser.Api/BaseApiService.cs - // TODO: Remove code duplication - /// - /// Gets the path segment at the specified index. - /// - /// The index of the path segment. - /// The path segment at the specified index. - /// Path doesn't contain enough segments. - /// Path doesn't start with the base url. - protected internal ReadOnlySpan GetPathValue(int index) - { - static void ThrowIndexOutOfRangeException() - => throw new IndexOutOfRangeException("Path doesn't contain enough segments."); - - static void ThrowInvalidDataException() - => throw new InvalidDataException("Path doesn't start with the base url."); - - ReadOnlySpan path = Request.PathInfo; - - // Remove the protocol part from the url - int pos = path.LastIndexOf("://"); - if (pos != -1) - { - path = path.Slice(pos + 3); - } - - // Remove the query string - pos = path.LastIndexOf('?'); - if (pos != -1) - { - path = path.Slice(0, pos); - } - - // Remove the domain - pos = path.IndexOf('/'); - if (pos != -1) - { - path = path.Slice(pos); - } - - // Remove base url - string baseUrl = _configurationManager.Configuration.BaseUrl; - int baseUrlLen = baseUrl.Length; - if (baseUrlLen != 0) - { - if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase)) - { - path = path.Slice(baseUrlLen); - } - else - { - // The path doesn't start with the base url, - // how did we get here? - ThrowInvalidDataException(); - } - } - - // Remove leading / - path = path.Slice(1); - - // Backwards compatibility - const string Emby = "emby/"; - if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase)) - { - path = path.Slice(Emby.Length); - } - - const string MediaBrowser = "mediabrowser/"; - if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase)) - { - path = path.Slice(MediaBrowser.Length); - } - - // Skip segments until we are at the right index - for (int i = 0; i < index; i++) - { - pos = path.IndexOf('/'); - if (pos == -1) - { - ThrowIndexOutOfRangeException(); - } - - path = path.Slice(pos + 1); - } - - // Remove the rest - pos = path.IndexOf('/'); - if (pos != -1) - { - path = path.Slice(0, pos); - } - - return path; - } - - public object Get(GetIcon request) - { - var contentType = "image/" + Path.GetExtension(request.Filename) - .TrimStart('.') - .ToLowerInvariant(); - - var cacheLength = TimeSpan.FromDays(365); - var cacheKey = Request.RawUrl.GetMD5(); - - return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream)); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Subscribe(ProcessContentDirectoryEventRequest request) - { - return ProcessEventRequest(ContentDirectory); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Subscribe(ProcessConnectionManagerEventRequest request) - { - return ProcessEventRequest(ConnectionManager); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request) - { - return ProcessEventRequest(MediaReceiverRegistrar); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Unsubscribe(ProcessContentDirectoryEventRequest request) - { - return ProcessEventRequest(ContentDirectory); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Unsubscribe(ProcessConnectionManagerEventRequest request) - { - return ProcessEventRequest(ConnectionManager); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request) - { - return ProcessEventRequest(MediaReceiverRegistrar); - } - - private object ProcessEventRequest(IEventManager eventManager) - { - var subscriptionId = GetHeader("SID"); - - if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase)) - { - var notificationType = GetHeader("NT"); - - var callback = GetHeader("CALLBACK"); - var timeoutString = GetHeader("TIMEOUT"); - - if (string.IsNullOrEmpty(notificationType)) - { - return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback)); - } - - return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback)); - } - - return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId)); - } - - private object GetSubscriptionResponse(EventSubscriptionResponse response) - { - return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers); - } - } -} diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index c5d60b2a05..c0d01f4480 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -57,11 +57,11 @@ namespace Emby.Dlna.Main private ISsdpCommunicationsServer _communicationsServer; - internal IContentDirectory ContentDirectory { get; private set; } + public IContentDirectory ContentDirectory { get; private set; } - internal IConnectionManager ConnectionManager { get; private set; } + public IConnectionManager ConnectionManager { get; private set; } - internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } + public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } public static DlnaEntryPoint Current; diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs new file mode 100644 index 0000000000..2fdd1e4899 --- /dev/null +++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Jellyfin.Api.Attributes +{ + /// + /// Identifies an action that supports the HTTP GET method. + /// + public class HttpSubscribeAttribute : HttpMethodAttribute + { + private static readonly IEnumerable _supportedMethods = new[] { "SUBSCRIBE" }; + + /// + /// Initializes a new instance of the class. + /// + public HttpSubscribeAttribute() + : base(_supportedMethods) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The route template. May not be null. + public HttpSubscribeAttribute(string template) + : base(_supportedMethods, template) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + } + } +} diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs new file mode 100644 index 0000000000..d6d7e4563d --- /dev/null +++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Jellyfin.Api.Attributes +{ + /// + /// Identifies an action that supports the HTTP GET method. + /// + public class HttpUnsubscribeAttribute : HttpMethodAttribute + { + private static readonly IEnumerable _supportedMethods = new[] { "UNSUBSCRIBE" }; + + /// + /// Initializes a new instance of the class. + /// + public HttpUnsubscribeAttribute() + : base(_supportedMethods) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The route template. May not be null. + public HttpUnsubscribeAttribute(string template) + : base(_supportedMethods, template) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + } + } +} diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs new file mode 100644 index 0000000000..731d6707cf --- /dev/null +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -0,0 +1,259 @@ +#nullable enable + +using System; +using System.IO; +using System.Threading.Tasks; +using Emby.Dlna; +using Emby.Dlna.Main; +using Jellyfin.Api.Attributes; +using MediaBrowser.Controller.Dlna; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +#pragma warning disable CA1801 + +namespace Jellyfin.Api.Controllers +{ + /// + /// Dlna Server Controller. + /// + [Route("Dlna")] + public class DlnaServerController : BaseJellyfinApiController + { + private const string XMLContentType = "text/xml; charset=UTF-8"; + + private readonly IDlnaManager _dlnaManager; + private readonly IContentDirectory _contentDirectory; + private readonly IConnectionManager _connectionManager; + private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public DlnaServerController(IDlnaManager dlnaManager) + { + _dlnaManager = dlnaManager; + _contentDirectory = DlnaEntryPoint.Current.ContentDirectory; + _connectionManager = DlnaEntryPoint.Current.ConnectionManager; + _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; + } + + /// + /// Get Description Xml. + /// + /// Server UUID. + /// Description Xml. + [HttpGet("{Uuid}/description.xml")] + [HttpGet("{Uuid}/description")] + [Produces(XMLContentType)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDescriptionXml([FromRoute] string uuid) + { + var url = GetAbsoluteUri(); + var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, uuid, serverAddress); + + // TODO GetStaticResult doesn't do anything special? + /* + var cacheLength = TimeSpan.FromDays(1); + var cacheKey = Request.Path.Value.GetMD5(); + var bytes = Encoding.UTF8.GetBytes(xml); + */ + return Ok(xml); + } + + /// + /// Gets Dlna content directory xml. + /// + /// Server UUID. + /// Dlna content directory xml. + [HttpGet("{Uuid}/ContentDirectory/ContentDirectory.xml")] + [HttpGet("{Uuid}/ContentDirectory/ContentDirectory")] + [Produces(XMLContentType)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetContentDirectory([FromRoute] string uuid) + { + return Ok(_contentDirectory.GetServiceXml()); + } + + /// + /// Gets Dlna media receiver registrar xml. + /// + /// Server UUID. + /// Dlna media receiver registrar xml. + [HttpGet("{Uuid}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")] + [HttpGet("{Uuid}/MediaReceiverRegistrar/MediaReceiverRegistrar")] + [Produces(XMLContentType)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetMediaReceiverRegistrar([FromRoute] string uuid) + { + return Ok(_mediaReceiverRegistrar.GetServiceXml()); + } + + /// + /// Gets Dlna media receiver registrar xml. + /// + /// Server UUID. + /// Dlna media receiver registrar xml. + [HttpGet("{Uuid}/ConnectionManager/ConnectionManager.xml")] + [HttpGet("{Uuid}/ConnectionManager/ConnectionManager")] + [Produces(XMLContentType)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetConnectionManager([FromRoute] string uuid) + { + return Ok(_connectionManager.GetServiceXml()); + } + + /// + /// Process a content directory control request. + /// + /// Server UUID. + /// Control response. + [HttpPost("{Uuid}/ContentDirectory/Control")] + public async Task> ProcessContentDirectoryControlRequest([FromRoute] string uuid) + { + var response = await PostAsync(uuid, Request.Body, _contentDirectory).ConfigureAwait(false); + return Ok(response); + } + + /// + /// Process a connection manager control request. + /// + /// Server UUID. + /// Control response. + [HttpPost("{Uuid}/ConnectionManager/Control")] + public async Task> ProcessConnectionManagerControlRequest([FromRoute] string uuid) + { + var response = await PostAsync(uuid, Request.Body, _connectionManager).ConfigureAwait(false); + return Ok(response); + } + + /// + /// Process a media receiver registrar control request. + /// + /// Server UUID. + /// Control response. + [HttpPost("{Uuid}/MediaReceiverRegistrar/Control")] + public async Task> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string uuid) + { + var response = await PostAsync(uuid, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); + return Ok(response); + } + + /// + /// Processes an event subscription request. + /// + /// Server UUID. + /// Event subscription response. + [HttpSubscribe("{Uuid}/MediaReceiverRegistrar/Events")] + [HttpUnsubscribe("{Uuid}/MediaReceiverRegistrar/Events")] + public ActionResult ProcessMediaReceiverRegistrarEventRequest(string uuid) + { + return Ok(ProcessEventRequest(_mediaReceiverRegistrar)); + } + + /// + /// Processes an event subscription request. + /// + /// Server UUID. + /// Event subscription response. + [HttpSubscribe("{Uuid}/ContentDirectory/Events")] + [HttpUnsubscribe("{Uuid}/ContentDirectory/Events")] + public ActionResult ProcessContentDirectoryEventRequest(string uuid) + { + return Ok(ProcessEventRequest(_contentDirectory)); + } + + /// + /// Processes an event subscription request. + /// + /// Server UUID. + /// Event subscription response. + [HttpSubscribe("{Uuid}/ConnectionManager/Events")] + [HttpUnsubscribe("{Uuid}/ConnectionManager/Events")] + public ActionResult ProcessConnectionManagerEventRequest(string uuid) + { + return Ok(ProcessEventRequest(_connectionManager)); + } + + /// + /// Gets a server icon. + /// + /// Server UUID. + /// The icon filename. + /// Icon stream. + [HttpGet("{Uuid}/icons/{Filename}")] + public ActionResult GetIconId([FromRoute] string uuid, [FromRoute] string fileName) + { + return GetIcon(fileName); + } + + /// + /// Gets a server icon. + /// + /// Server UUID. + /// The icon filename. + /// Icon stream. + [HttpGet("icons/{Filename}")] + public ActionResult GetIcon([FromQuery] string uuid, [FromRoute] string fileName) + { + return GetIcon(fileName); + } + + private ActionResult GetIcon(string fileName) + { + var icon = _dlnaManager.GetIcon(fileName); + if (icon == null) + { + return NotFound(); + } + + var contentType = "image/" + Path.GetExtension(fileName) + .TrimStart('.') + .ToLowerInvariant(); + + return new FileStreamResult(icon.Stream, contentType); + } + + private string GetAbsoluteUri() + { + return $"{Request.Scheme}://{Request.Host}{Request.Path}"; + } + + private Task PostAsync(string id, Stream requestStream, IUpnpService service) + { + return service.ProcessControlRequestAsync(new ControlRequest + { + Headers = Request.Headers, + InputXml = requestStream, + TargetServerUuId = id, + RequestedUrl = GetAbsoluteUri() + }); + } + + private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager) + { + var subscriptionId = Request.Headers["SID"]; + if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase)) + { + var notificationType = Request.Headers["NT"]; + var callback = Request.Headers["CALLBACK"]; + var timeoutString = Request.Headers["TIMEOUT"]; + + if (string.IsNullOrEmpty(notificationType)) + { + return eventManager.RenewEventSubscription( + subscriptionId, + notificationType, + timeoutString, + callback); + } + + return eventManager.CreateEventSubscription(notificationType, timeoutString, callback); + } + + return eventManager.CancelEventSubscription(subscriptionId); + } + } +} diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 8f23ef9d03..a2e116fd7c 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -14,6 +14,7 @@ + From 2a49b19a7c02f16cd4bb1d847c1ff76c5df316fb Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 22 Apr 2020 00:21:37 -0600 Subject: [PATCH 061/724] Update documentation of startIndex Co-Authored-By: Vasily --- Jellyfin.Api/Controllers/NotificationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 6145246ed3..bb9f5a7b3c 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers /// /// The user's ID. /// An optional filter by notification read state. - /// The optional index to start at. All notifications with a lower index will be dropped from the results. + /// The optional index to start at. All notifications with a lower index will be omitted from the results. /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] From 7693cc0db006ef4eb3a90d161b14ac4551bb96a7 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 22 Apr 2020 10:00:10 -0600 Subject: [PATCH 062/724] Use ActionResult return type for all endpoints --- .../Controllers/NotificationsController.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index bb9f5a7b3c..0bf3aa1b47 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -43,8 +43,8 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - [ProducesResponseType(typeof(NotificationResultDto), StatusCodes.Status200OK)] - public NotificationResultDto GetNotifications( + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, @@ -59,8 +59,8 @@ namespace Jellyfin.Api.Controllers /// The user's ID. /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] - [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)] - public NotificationsSummaryDto GetNotificationsSummary( + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNotificationsSummary( [FromRoute] string userId) { return new NotificationsSummaryDto(); @@ -71,8 +71,8 @@ namespace Jellyfin.Api.Controllers /// /// All notification types. [HttpGet("Types")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IEnumerable GetNotificationTypes() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetNotificationTypes() { return _notificationManager.GetNotificationTypes(); } @@ -82,10 +82,10 @@ namespace Jellyfin.Api.Controllers /// /// All notification services. [HttpGet("Services")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IEnumerable GetNotificationServices() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetNotificationServices() { - return _notificationManager.GetNotificationServices(); + return _notificationManager.GetNotificationServices().ToList(); } /// @@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers /// The level of the notification. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - public void CreateAdminNotification( + public ActionResult CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, [FromQuery] string? url, @@ -114,6 +114,8 @@ namespace Jellyfin.Api.Controllers }; _notificationManager.SendNotification(notification, CancellationToken.None); + + return Ok(); } /// @@ -123,10 +125,11 @@ namespace Jellyfin.Api.Controllers /// A comma-separated list of the IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] - public void SetRead( + public ActionResult SetRead( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } /// @@ -136,10 +139,11 @@ namespace Jellyfin.Api.Controllers /// A comma-separated list of the IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] - public void SetUnread( + public ActionResult SetUnread( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } } } From a06d271725f6e746d9a970f29283ab8f3ebae607 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 22 Apr 2020 13:07:21 -0600 Subject: [PATCH 063/724] Move ConfigurationService to Jellyfin.Api --- .../Controllers/ConfigurationController.cs | 128 +++++++++++++++ .../ConfigurationDtos/MediaEncoderPathDto.cs | 18 +++ MediaBrowser.Api/ConfigurationService.cs | 146 ------------------ 3 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ConfigurationController.cs create mode 100644 Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs delete mode 100644 MediaBrowser.Api/ConfigurationService.cs diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs new file mode 100644 index 0000000000..14e45833f0 --- /dev/null +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -0,0 +1,128 @@ +#nullable enable + +using System.Threading.Tasks; +using Jellyfin.Api.Models.ConfigurationDtos; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Configuration Controller. + /// + [Route("System")] + [Authenticated] + public class ConfigurationController : BaseJellyfinApiController + { + 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) + { + _configurationManager = configurationManager; + _mediaEncoder = mediaEncoder; + _jsonSerializer = jsonSerializer; + } + + /// + /// Gets application configuration. + /// + /// Application configuration. + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetConfiguration() + { + return Ok(_configurationManager.Configuration); + } + + /// + /// Updates application configuration. + /// + /// Configuration. + /// Status. + [HttpPost("Configuration")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) + { + _configurationManager.ReplaceConfiguration(configuration); + return Ok(); + } + + /// + /// Gets a named configuration. + /// + /// Configuration key. + /// Configuration. + [HttpGet("Configuration/{Key}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNamedConfiguration([FromRoute] string key) + { + return Ok(_configurationManager.GetConfiguration(key)); + } + + /// + /// Updates named configuration. + /// + /// Configuration key. + /// Status. + [HttpPost("Configuration/{Key}")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + 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(); + } + + /// + /// Gets a default MetadataOptions object. + /// + /// MetadataOptions. + [HttpGet("Configuration/MetadataOptions/Default")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultMetadataOptions() + { + return Ok(new MetadataOptions()); + } + + /// + /// Updates the path to the media encoder. + /// + /// Media encoder path form body. + /// Status. + [HttpPost("MediaEncoder/Path")] + [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) + { + _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); + return Ok(); + } + } +} diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs new file mode 100644 index 0000000000..b05e0cdf5a --- /dev/null +++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.ConfigurationDtos +{ + /// + /// Media Encoder Path Dto. + /// + public class MediaEncoderPathDto + { + /// + /// Gets or sets media encoder path. + /// + public string Path { get; set; } + + /// + /// Gets or sets media encoder path type. + /// + public string PathType { get; set; } + } +} diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs deleted file mode 100644 index 316be04a03..0000000000 --- a/MediaBrowser.Api/ConfigurationService.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetConfiguration - /// - [Route("/System/Configuration", "GET", Summary = "Gets application configuration")] - [Authenticated] - public class GetConfiguration : IReturn - { - - } - - [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")] - [Authenticated(AllowBeforeStartupWizard = true)] - public class GetNamedConfiguration - { - [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Key { get; set; } - } - - /// - /// Class UpdateConfiguration - /// - [Route("/System/Configuration", "POST", Summary = "Updates application configuration")] - [Authenticated(Roles = "Admin")] - public class UpdateConfiguration : ServerConfiguration, IReturnVoid - { - } - - [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")] - [Authenticated(Roles = "Admin")] - public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream - { - [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Key { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")] - [Authenticated(Roles = "Admin")] - public class GetDefaultMetadataOptions : IReturn - { - - } - - [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")] - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] - public class UpdateMediaEncoderPath : IReturnVoid - { - [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Path { get; set; } - [ApiMember(Name = "PathType", Description = "PathType", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string PathType { get; set; } - } - - public class ConfigurationService : BaseApiService - { - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// The _configuration manager - /// - private readonly IServerConfigurationManager _configurationManager; - - private readonly IMediaEncoder _mediaEncoder; - - public ConfigurationService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IJsonSerializer jsonSerializer, - IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _jsonSerializer = jsonSerializer; - _configurationManager = configurationManager; - _mediaEncoder = mediaEncoder; - } - - public void Post(UpdateMediaEncoderPath request) - { - _mediaEncoder.UpdateEncoderPath(request.Path, request.PathType); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetConfiguration request) - { - return ToOptimizedResult(_configurationManager.Configuration); - } - - public object Get(GetNamedConfiguration request) - { - var result = _configurationManager.GetConfiguration(request.Key); - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified configuraiton. - /// - /// The request. - public void Post(UpdateConfiguration request) - { - // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration - var json = _jsonSerializer.SerializeToString(request); - - var config = _jsonSerializer.DeserializeFromString(json); - - _configurationManager.ReplaceConfiguration(config); - } - - public async Task Post(UpdateNamedConfiguration request) - { - var key = GetPathValue(2).ToString(); - - var configurationType = _configurationManager.GetConfigurationType(key); - var configuration = await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, configurationType).ConfigureAwait(false); - - _configurationManager.SaveConfiguration(key, configuration); - } - - public object Get(GetDefaultMetadataOptions request) - { - return ToOptimizedResult(new MetadataOptions()); - } - } -} From c6eebca335d09d6a6c627205e126448ab5441f37 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:29:28 -0600 Subject: [PATCH 064/724] Apply suggestions and add URL to log message --- .../Middleware/ExceptionMiddleware.cs | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 0d9dac89f0..ecc76594e4 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Net.Mime; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -22,15 +23,15 @@ namespace Jellyfin.Server.Middleware /// Initializes a new instance of the class. /// /// Next request delegate. - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. public ExceptionMiddleware( RequestDelegate next, - ILoggerFactory loggerFactory, + ILogger logger, IServerConfigurationManager serverConfigurationManager) { _next = next; - _logger = loggerFactory.CreateLogger(); + _logger = logger; _configuration = serverConfigurationManager; } @@ -54,9 +55,14 @@ namespace Jellyfin.Server.Middleware } ex = GetActualException(ex); - _logger.LogError(ex, "Error processing request: {0}", ex.Message); + _logger.LogError( + ex, + "Error processing request: {ExceptionMessage}. URL {Method} {Url}. ", + ex.Message, + context.Request.Method, + context.Request.Path); context.Response.StatusCode = GetStatusCode(ex); - context.Response.ContentType = "text/plain"; + context.Response.ContentType = MediaTypeNames.Text.Plain; var errorContent = NormalizeExceptionMessage(ex.Message); await context.Response.WriteAsync(errorContent).ConfigureAwait(false); @@ -105,16 +111,14 @@ namespace Jellyfin.Server.Middleware } // Strip any information we don't want to reveal - msg = msg.Replace( - _configuration.ApplicationPaths.ProgramSystemPath, - string.Empty, - StringComparison.OrdinalIgnoreCase); - msg = msg.Replace( - _configuration.ApplicationPaths.ProgramDataPath, - string.Empty, - StringComparison.OrdinalIgnoreCase); - - return msg; + return msg.Replace( + _configuration.ApplicationPaths.ProgramSystemPath, + string.Empty, + StringComparison.OrdinalIgnoreCase) + .Replace( + _configuration.ApplicationPaths.ProgramDataPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); } } } From c7c2f9da90a7cf7a452de0ab1adf7e36f422bbe1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:51:04 -0600 Subject: [PATCH 065/724] Apply suggestions --- Jellyfin.Api/Controllers/StartupController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 2db7e32aad..14c59593fb 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage }; - return Ok(result); + return result; } /// @@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetFirstUser() { var user = _userManager.Users.First(); - return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); + return new StartupUserDto { Name = user.Name, Password = user.Password }; } /// From 4d894c4344fd23026bbfdc0a1cdd24231441a444 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:55:47 -0600 Subject: [PATCH 066/724] 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 1223eb5a2285c48f50b07fb5aa2c463928b69afe Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 08:03:41 -0600 Subject: [PATCH 067/724] Remove unneeded Ok calls. --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 25391bcf84..42e87edd6f 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return Ok(result); + return result; } /// From 5ca7e1fd79d85e7e531c747f4eca203ff862be8d Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 08:54:28 -0600 Subject: [PATCH 068/724] Move ChannelService to Jellyfin.Api --- .../Controllers/ChannelsController.cs | 238 ++++++++++++ Jellyfin.Api/Extensions/RequestExtensions.cs | 90 +++++ MediaBrowser.Api/ChannelService.cs | 341 ------------------ 3 files changed, 328 insertions(+), 341 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ChannelsController.cs create mode 100644 Jellyfin.Api/Extensions/RequestExtensions.cs delete mode 100644 MediaBrowser.Api/ChannelService.cs diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs new file mode 100644 index 0000000000..4e2621b7b3 --- /dev/null +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -0,0 +1,238 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Extensions; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Channels Controller. + /// + public class ChannelsController : BaseJellyfinApiController + { + private readonly IChannelManager _channelManager; + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public ChannelsController(IChannelManager channelManager, IUserManager userManager) + { + _channelManager = channelManager; + _userManager = userManager; + } + + /// + /// Gets available channels. + /// + /// User Id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Filter by channels that support getting latest items. + /// Optional. Filter by channels that support media deletion. + /// Optional. Filter by channels that are favorite. + /// Channels. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetChannels( + [FromQuery] Guid userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? supportsLatestItems, + [FromQuery] bool? supportsMediaDeletion, + [FromQuery] bool? isFavorite) + { + return _channelManager.GetChannels(new ChannelQuery + { + Limit = limit, + StartIndex = startIndex, + UserId = userId, + SupportsLatestItems = supportsLatestItems, + SupportsMediaDeletion = supportsMediaDeletion, + IsFavorite = isFavorite + }); + } + + /// + /// Get all channel features. + /// + /// Channel features. + [HttpGet("Features")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetAllChannelFeatures() + { + return _channelManager.GetAllChannelFeatures(); + } + + /// + /// Get channel features. + /// + /// Channel id. + /// Channel features. + [HttpGet("{Id}/Features")] + public ActionResult GetChannelFeatures([FromRoute] string id) + { + return _channelManager.GetChannelFeatures(id); + } + + /// + /// Get channel items. + /// + /// Channel Id. + /// Folder Id. + /// User Id. + /// 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. + /// Sort Order - Ascending,Descending. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Channel items. + [HttpGet("{Id}/Items")] + public async Task>> GetChannelItems( + [FromRoute] Guid id, + [FromQuery] Guid? folderId, + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string sortOrder, + [FromQuery] string filters, + [FromQuery] string sortBy, + [FromQuery] string fields) + { + var user = userId == null + ? null + : _userManager.GetUserById(userId.Value); + + var query = new InternalItemsQuery(user) + { + Limit = limit, + StartIndex = startIndex, + ChannelIds = new[] { id }, + ParentId = folderId ?? Guid.Empty, + OrderBy = RequestExtensions.GetOrderBy(sortBy, sortOrder), + DtoOptions = new DtoOptions { Fields = RequestExtensions.GetItemFields(fields) } + }; + + foreach (var filter in RequestExtensions.GetFilters(filters)) + { + switch (filter) + { + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + } + } + + return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets latest channel items. + /// + /// User Id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. Specify one or more channel id's, comma delimited. + /// Latest channel items. + public async Task>> GetLatestChannelItems( + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string filters, + [FromQuery] string fields, + [FromQuery] string channelIds) + { + var user = userId == null + ? null + : _userManager.GetUserById(userId.Value); + + var query = new InternalItemsQuery(user) + { + Limit = limit, + StartIndex = startIndex, + ChannelIds = + (channelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => new Guid(i)).ToArray(), + DtoOptions = new DtoOptions { Fields = RequestExtensions.GetItemFields(fields) } + }; + + foreach (var filter in RequestExtensions.GetFilters(filters)) + { + switch (filter) + { + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + } + } + + return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Extensions/RequestExtensions.cs b/Jellyfin.Api/Extensions/RequestExtensions.cs new file mode 100644 index 0000000000..b9d11dfefa --- /dev/null +++ b/Jellyfin.Api/Extensions/RequestExtensions.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Extensions +{ + /// + /// Request Extensions. + /// + public static class RequestExtensions + { + /// + /// Get Order By. + /// + /// Sort By. Comma delimited string. + /// Sort Order. Comma delimited string. + /// Order By. + public static ValueTuple[] GetOrderBy(string sortBy, string requestedSortOrder) + { + var val = sortBy; + + if (string.IsNullOrEmpty(val)) + { + return Array.Empty>(); + } + + var vals = val.Split(','); + if (string.IsNullOrWhiteSpace(requestedSortOrder)) + { + requestedSortOrder = "Ascending"; + } + + var sortOrders = requestedSortOrder.Split(','); + + var result = new ValueTuple[vals.Length]; + + for (var i = 0; i < vals.Length; i++) + { + var sortOrderIndex = sortOrders.Length > i ? i : 0; + + var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null; + var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) + ? SortOrder.Descending + : SortOrder.Ascending; + + result[i] = new ValueTuple(vals[i], sortOrder); + } + + return result; + } + + /// + /// Gets the item fields. + /// + /// The fields. + /// IEnumerable{ItemFields}. + public static ItemFields[] GetItemFields(string fields) + { + if (string.IsNullOrEmpty(fields)) + { + return Array.Empty(); + } + + return fields.Split(',').Select(v => + { + if (Enum.TryParse(v, true, out ItemFields value)) + { + return (ItemFields?)value; + } + + return null; + }).Where(i => i.HasValue).Select(i => i.Value).ToArray(); + } + + /// + /// Get parsed filters. + /// + /// The filters. + /// Item filters. + public static IEnumerable GetFilters(string filters) + { + return string.IsNullOrEmpty(filters) + ? Array.Empty() + : filters.Split(',').Select(v => Enum.Parse(v, true)); + } + } +} diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs deleted file mode 100644 index fd9b8c3968..0000000000 --- a/MediaBrowser.Api/ChannelService.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Api.UserLibrary; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Channels; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Channels", "GET", Summary = "Gets available channels")] - public class GetChannels : IReturn> - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// 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 = "SupportsLatestItems", Description = "Optional. Filter by channels that support getting latest items.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? SupportsLatestItems { get; set; } - - public bool? SupportsMediaDeletion { get; set; } - - /// - /// Gets or sets a value indicating whether this instance is favorite. - /// - /// null if [is favorite] contains no value, true if [is favorite]; otherwise, false. - public bool? IsFavorite { get; set; } - } - - [Route("/Channels/{Id}/Features", "GET", Summary = "Gets features for a channel")] - public class GetChannelFeatures : IReturn - { - [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Channels/Features", "GET", Summary = "Gets features for a channel")] - public class GetAllChannelFeatures : IReturn - { - } - - [Route("/Channels/{Id}/Items", "GET", Summary = "Gets channel items")] - public class GetChannelItems : IReturn>, IHasItemFields - { - [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - [ApiMember(Name = "FolderId", Description = "Folder Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string FolderId { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// 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 = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string SortOrder { get; set; } - - [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string Filters { get; set; } - - [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string SortBy { get; set; } - - [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string Fields { get; set; } - - /// - /// Gets the filters. - /// - /// IEnumerable{ItemFilter}. - public IEnumerable GetFilters() - { - var val = Filters; - - return string.IsNullOrEmpty(val) - ? Array.Empty() - : val.Split(',').Select(v => Enum.Parse(v, true)); - } - - /// - /// Gets the order by. - /// - /// IEnumerable{ItemSortBy}. - public ValueTuple[] GetOrderBy() - { - return BaseItemsRequest.GetOrderBy(SortBy, SortOrder); - } - } - - [Route("/Channels/Items/Latest", "GET", Summary = "Gets channel items")] - public class GetLatestChannelItems : IReturn>, IHasItemFields - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// 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 = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string Filters { get; set; } - - [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string Fields { get; set; } - - [ApiMember(Name = "ChannelIds", Description = "Optional. Specify one or more channel id's, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string ChannelIds { get; set; } - - /// - /// Gets the filters. - /// - /// IEnumerable{ItemFilter}. - public IEnumerable GetFilters() - { - return string.IsNullOrEmpty(Filters) - ? Array.Empty() - : Filters.Split(',').Select(v => Enum.Parse(v, true)); - } - } - - [Authenticated] - public class ChannelService : BaseApiService - { - private readonly IChannelManager _channelManager; - private IUserManager _userManager; - - public ChannelService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IChannelManager channelManager, - IUserManager userManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _channelManager = channelManager; - _userManager = userManager; - } - - public object Get(GetAllChannelFeatures request) - { - var result = _channelManager.GetAllChannelFeatures(); - - return ToOptimizedResult(result); - } - - public object Get(GetChannelFeatures request) - { - var result = _channelManager.GetChannelFeatures(request.Id); - - return ToOptimizedResult(result); - } - - public object Get(GetChannels request) - { - var result = _channelManager.GetChannels(new ChannelQuery - { - Limit = request.Limit, - StartIndex = request.StartIndex, - UserId = request.UserId, - SupportsLatestItems = request.SupportsLatestItems, - SupportsMediaDeletion = request.SupportsMediaDeletion, - IsFavorite = request.IsFavorite - }); - - return ToOptimizedResult(result); - } - - public async Task Get(GetChannelItems request) - { - var user = request.UserId.Equals(Guid.Empty) - ? null - : _userManager.GetUserById(request.UserId); - - var query = new InternalItemsQuery(user) - { - Limit = request.Limit, - StartIndex = request.StartIndex, - ChannelIds = new[] { new Guid(request.Id) }, - ParentId = string.IsNullOrWhiteSpace(request.FolderId) ? Guid.Empty : new Guid(request.FolderId), - OrderBy = request.GetOrderBy(), - DtoOptions = new Controller.Dto.DtoOptions - { - Fields = request.GetItemFields() - } - - }; - - foreach (var filter in request.GetFilters()) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } - - var result = await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Get(GetLatestChannelItems request) - { - var user = request.UserId.Equals(Guid.Empty) - ? null - : _userManager.GetUserById(request.UserId); - - var query = new InternalItemsQuery(user) - { - Limit = request.Limit, - StartIndex = request.StartIndex, - ChannelIds = (request.ChannelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToArray(), - DtoOptions = new Controller.Dto.DtoOptions - { - Fields = request.GetItemFields() - } - }; - - foreach (var filter in request.GetFilters()) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } - - var result = await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - } -} From bb8e738a0817be2e13a8b21929d0f0aeb0c6a461 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:03:54 -0600 Subject: [PATCH 069/724] Fix Authorize attributes --- .../Controllers/ConfigurationController.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 14e45833f0..b508ac0547 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,12 +1,13 @@ #nullable enable using System.Threading.Tasks; +using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -17,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// Configuration Controller. /// [Route("System")] - [Authenticated] + [Authorize] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; @@ -48,7 +49,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetConfiguration() { - return Ok(_configurationManager.Configuration); + return _configurationManager.Configuration; } /// @@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers /// Configuration. /// Status. [HttpPost("Configuration")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) { @@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNamedConfiguration([FromRoute] string key) { - return Ok(_configurationManager.GetConfiguration(key)); + return _configurationManager.GetConfiguration(key); } /// @@ -83,7 +84,7 @@ namespace Jellyfin.Api.Controllers /// Configuration key. /// Status. [HttpPost("Configuration/{Key}")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UpdateNamedConfiguration([FromRoute] string key) { @@ -104,11 +105,11 @@ namespace Jellyfin.Api.Controllers /// /// MetadataOptions. [HttpGet("Configuration/MetadataOptions/Default")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDefaultMetadataOptions() { - return Ok(new MetadataOptions()); + return new MetadataOptions(); } /// @@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers /// Media encoder path form body. /// Status. [HttpPost("MediaEncoder/Path")] - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) { From f3da5dc8b7fef7e5fdeddff941c6d99063a1fd97 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:04:37 -0600 Subject: [PATCH 070/724] 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 311f2e2bc317cea7ac4d4cc783b961793bb997d5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:07:21 -0600 Subject: [PATCH 071/724] Fix Authorize attributes --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 42e87edd6f..0d375e668a 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -2,9 +2,9 @@ using System.ComponentModel.DataAnnotations; using System.Threading; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -14,7 +14,7 @@ namespace Jellyfin.Api.Controllers /// /// Display Preferences Controller. /// - [Authenticated] + [Authorize] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesRepository _displayPreferencesRepository; From 3c34d956088430da08bdd812c05d6a87c3bf9d25 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:23:29 -0600 Subject: [PATCH 072/724] Address comments --- .../Middleware/ExceptionMiddleware.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index ecc76594e4..6ebe015030 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -1,8 +1,10 @@ using System; using System.IO; using System.Net.Mime; +using System.Net.Sockets; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; @@ -55,15 +57,35 @@ namespace Jellyfin.Server.Middleware } ex = GetActualException(ex); - _logger.LogError( - ex, - "Error processing request: {ExceptionMessage}. URL {Method} {Url}. ", - ex.Message, - context.Request.Method, - context.Request.Path); + + bool ignoreStackTrace = + ex is SocketException + || ex is IOException + || ex is OperationCanceledException + || ex is SecurityException + || ex is AuthenticationException + || ex is FileNotFoundException; + + if (ignoreStackTrace) + { + _logger.LogError( + "Error processing request: {ExceptionMessage}. URL {Method} {Url}.", + ex.Message.TrimEnd('.'), + context.Request.Method, + context.Request.Path); + } + else + { + _logger.LogError( + ex, + "Error processing request. URL {Method} {Url}.", + ex.Message.TrimEnd('.'), + context.Request.Method, + context.Request.Path); + } + context.Response.StatusCode = GetStatusCode(ex); context.Response.ContentType = MediaTypeNames.Text.Plain; - var errorContent = NormalizeExceptionMessage(ex.Message); await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } From be50fae38a27878cb520e20ed7956a72d7b05a84 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:24:40 -0600 Subject: [PATCH 073/724] Address comments --- .../Extensions/ApiApplicationBuilderExtensions.cs | 10 ---------- Jellyfin.Server/Startup.cs | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 6c105ab65b..0bd654c7dc 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -24,15 +24,5 @@ namespace Jellyfin.Server.Extensions c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1"); }); } - - /// - /// Adds exception middleware to the application pipeline. - /// - /// The application builder. - /// The updated application builder. - public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder applicationBuilder) - { - return applicationBuilder.UseMiddleware(); - } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 7a632f6c44..b17357fc3d 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,4 +1,5 @@ using Jellyfin.Server.Extensions; +using Jellyfin.Server.Middleware; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -58,7 +59,7 @@ namespace Jellyfin.Server app.UseDeveloperExceptionPage(); } - app.UseExceptionMiddleware(); + app.UseMiddleware(); app.UseWebSockets(); From b8508a57d8320085c01a7e2d4656b233169584f2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:38:40 -0600 Subject: [PATCH 074/724] oop --- Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 6ebe015030..0d79bbfaff 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -79,7 +79,6 @@ namespace Jellyfin.Server.Middleware _logger.LogError( ex, "Error processing request. URL {Method} {Url}.", - ex.Message.TrimEnd('.'), context.Request.Method, context.Request.Path); } From 0765ef8bda4d23e33fde7a1bfe49b5a365c6d28e Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:41:10 -0600 Subject: [PATCH 075/724] Apply suggestions, fix warning --- Jellyfin.Server/Models/JsonOptions.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs index fa503bc9a4..2f0df3d2c7 100644 --- a/Jellyfin.Server/Models/JsonOptions.cs +++ b/Jellyfin.Server/Models/JsonOptions.cs @@ -7,11 +7,6 @@ namespace Jellyfin.Server.Models /// public static class JsonOptions { - /// - /// Base Json Serializer Options. - /// - private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions(); - /// /// Gets CamelCase json options. /// @@ -19,7 +14,7 @@ namespace Jellyfin.Server.Models { get { - var options = _jsonOptions; + var options = DefaultJsonOptions; options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; return options; } @@ -32,10 +27,15 @@ namespace Jellyfin.Server.Models { get { - var options = _jsonOptions; + var options = DefaultJsonOptions; options.PropertyNamingPolicy = null; return options; } } + + /// + /// Gets base Json Serializer Options. + /// + private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions(); } } From 85853f9ce3d77469b84e3334d7080cd025474ee8 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Fri, 24 Apr 2020 17:11:11 -0600 Subject: [PATCH 076/724] Add back in return type documentation --- Jellyfin.Api/Controllers/NotificationsController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 0bf3aa1b47..8da2a6c536 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -95,6 +95,7 @@ namespace Jellyfin.Api.Controllers /// The description of the notification. /// The URL of the notification. /// The level of the notification. + /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CreateAdminNotification( @@ -123,6 +124,7 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. + /// Status. [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetRead( @@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. + /// Status. [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetUnread( From 714aaefbcc3abf0b952efc831001cf42e1c873b0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 24 Apr 2020 18:20:36 -0600 Subject: [PATCH 077/724] Transfer EnvironmentService to Jellyfin.Api --- .../Controllers/EnvironmentController.cs | 200 ++++++++++++ .../DefaultDirectoryBrowserInfo.cs | 13 + .../Models/EnvironmentDtos/ValidatePathDto.cs | 23 ++ MediaBrowser.Api/EnvironmentService.cs | 296 ------------------ 4 files changed, 236 insertions(+), 296 deletions(-) create mode 100644 Jellyfin.Api/Controllers/EnvironmentController.cs create mode 100644 Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs create mode 100644 Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs delete mode 100644 MediaBrowser.Api/EnvironmentService.cs diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs new file mode 100644 index 0000000000..139c1af083 --- /dev/null +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -0,0 +1,200 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.EnvironmentDtos; +using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Environment Controller. + /// + [Authorize(Policy = Policies.RequiresElevation)] + public class EnvironmentController : BaseJellyfinApiController + { + private const char UncSeparator = '\\'; + private const string UncSeparatorString = "\\"; + + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public EnvironmentController(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + /// + /// Gets the contents of a given directory in the file system. + /// + /// The path. + /// An optional filter to include or exclude files from the results. true/false. + /// An optional filter to include or exclude folders from the results. true/false. + /// File system entries. + [HttpGet("DirectoryContents")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetDirectoryContents( + [FromQuery, BindRequired] string path, + [FromQuery] bool includeFiles, + [FromQuery] bool includeDirectories) + { + const string networkPrefix = UncSeparatorString + UncSeparatorString; + if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase) + && path.LastIndexOf(UncSeparator) == 1) + { + return Array.Empty(); + } + + var entries = _fileSystem.GetFileSystemEntries(path).OrderBy(i => i.FullName).Where(i => + { + var isDirectory = i.IsDirectory; + + if (!includeFiles && !isDirectory) + { + return false; + } + + return includeDirectories || !isDirectory; + }); + + return entries.Select(f => new FileSystemEntryInfo + { + Name = f.Name, + Path = f.FullName, + Type = f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File + }); + } + + /// + /// Validates path. + /// + /// Validate request object. + /// Status. + [HttpPost("ValidatePath")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult ValidatePath([FromBody, BindRequired] ValidatePathDto validatePathDto) + { + if (validatePathDto.IsFile.HasValue) + { + if (validatePathDto.IsFile.Value) + { + if (!System.IO.File.Exists(validatePathDto.Path)) + { + return NotFound(); + } + } + else + { + if (!Directory.Exists(validatePathDto.Path)) + { + return NotFound(); + } + } + } + else + { + if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path)) + { + return NotFound(); + } + + if (validatePathDto.ValidateWritable) + { + var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString()); + try + { + System.IO.File.WriteAllText(file, string.Empty); + } + finally + { + if (System.IO.File.Exists(file)) + { + System.IO.File.Delete(file); + } + } + } + } + + return Ok(); + } + + /// + /// Gets network paths. + /// + /// List of entries. + [Obsolete("This endpoint is obsolete.")] + [HttpGet("NetworkShares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetNetworkShares() + { + return Array.Empty(); + } + + /// + /// Gets available drives from the server's file system. + /// + /// List of entries. + [HttpGet("Drives")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetDrives() + { + return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo + { + Name = d.Name, + Path = d.FullName, + Type = FileSystemEntryType.Directory + }); + } + + /// + /// Gets the parent path of a given path. + /// + /// The path. + /// Parent path. + [HttpGet("ParentPath")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetParentPath([FromQuery, BindRequired] string path) + { + string? parent = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(parent)) + { + // Check if unc share + var index = path.LastIndexOf(UncSeparator); + + if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0) + { + parent = path.Substring(0, index); + + if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator))) + { + parent = null; + } + } + } + + return parent; + } + + /// + /// Get Default directory browser. + /// + /// Default directory browser. + [HttpGet("DefaultDirectoryBrowser")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultDirectoryBrowser() + { + return new DefaultDirectoryBrowserInfo(); + } + } +} diff --git a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs new file mode 100644 index 0000000000..6b1c750bf6 --- /dev/null +++ b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs @@ -0,0 +1,13 @@ +namespace Jellyfin.Api.Models.EnvironmentDtos +{ + /// + /// Default directory browser info. + /// + public class DefaultDirectoryBrowserInfo + { + /// + /// Gets or sets the path. + /// + public string Path { get; set; } + } +} diff --git a/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs new file mode 100644 index 0000000000..60c82e166b --- /dev/null +++ b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs @@ -0,0 +1,23 @@ +namespace Jellyfin.Api.Models.EnvironmentDtos +{ + /// + /// Validate path object. + /// + public class ValidatePathDto + { + /// + /// Gets or sets a value indicating whether validate if path is writable. + /// + public bool ValidateWritable { get; set; } + + /// + /// Gets or sets the path. + /// + public string Path { get; set; } + + /// + /// Gets or sets is path file. + /// + public bool? IsFile { get; set; } + } +} diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs deleted file mode 100644 index d199ce1544..0000000000 --- a/MediaBrowser.Api/EnvironmentService.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetDirectoryContents - /// - [Route("/Environment/DirectoryContents", "GET", Summary = "Gets the contents of a given directory in the file system")] - public class GetDirectoryContents : IReturn> - { - /// - /// Gets or sets the path. - /// - /// The path. - [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Path { get; set; } - - /// - /// Gets or sets a value indicating whether [include files]. - /// - /// true if [include files]; otherwise, false. - [ApiMember(Name = "IncludeFiles", Description = "An optional filter to include or exclude files from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool IncludeFiles { get; set; } - - /// - /// Gets or sets a value indicating whether [include directories]. - /// - /// true if [include directories]; otherwise, false. - [ApiMember(Name = "IncludeDirectories", Description = "An optional filter to include or exclude folders from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool IncludeDirectories { get; set; } - } - - [Route("/Environment/ValidatePath", "POST", Summary = "Gets the contents of a given directory in the file system")] - public class ValidatePath - { - /// - /// Gets or sets the path. - /// - /// The path. - [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Path { get; set; } - - public bool ValidateWriteable { get; set; } - public bool? IsFile { get; set; } - } - - [Obsolete] - [Route("/Environment/NetworkShares", "GET", Summary = "Gets shares from a network device")] - public class GetNetworkShares : IReturn> - { - /// - /// Gets or sets the path. - /// - /// The path. - [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Path { get; set; } - } - - /// - /// Class GetDrives - /// - [Route("/Environment/Drives", "GET", Summary = "Gets available drives from the server's file system")] - public class GetDrives : IReturn> - { - } - - /// - /// Class GetNetworkComputers - /// - [Route("/Environment/NetworkDevices", "GET", Summary = "Gets a list of devices on the network")] - public class GetNetworkDevices : IReturn> - { - } - - [Route("/Environment/ParentPath", "GET", Summary = "Gets the parent path of a given path")] - public class GetParentPath : IReturn - { - /// - /// Gets or sets the path. - /// - /// The path. - [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Path { get; set; } - } - - public class DefaultDirectoryBrowserInfo - { - public string Path { get; set; } - } - - [Route("/Environment/DefaultDirectoryBrowser", "GET", Summary = "Gets the parent path of a given path")] - public class GetDefaultDirectoryBrowser : IReturn - { - - } - - /// - /// Class EnvironmentService - /// - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] - public class EnvironmentService : BaseApiService - { - private const char UncSeparator = '\\'; - private const string UncSeparatorString = "\\"; - - /// - /// The _network manager - /// - private readonly INetworkManager _networkManager; - private readonly IFileSystem _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The network manager. - public EnvironmentService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - INetworkManager networkManager, - IFileSystem fileSystem) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _networkManager = networkManager; - _fileSystem = fileSystem; - } - - public void Post(ValidatePath request) - { - if (request.IsFile.HasValue) - { - if (request.IsFile.Value) - { - if (!File.Exists(request.Path)) - { - throw new FileNotFoundException("File not found", request.Path); - } - } - else - { - if (!Directory.Exists(request.Path)) - { - throw new FileNotFoundException("File not found", request.Path); - } - } - } - - else - { - if (!File.Exists(request.Path) && !Directory.Exists(request.Path)) - { - throw new FileNotFoundException("Path not found", request.Path); - } - - if (request.ValidateWriteable) - { - EnsureWriteAccess(request.Path); - } - } - } - - protected void EnsureWriteAccess(string path) - { - var file = Path.Combine(path, Guid.NewGuid().ToString()); - - File.WriteAllText(file, string.Empty); - _fileSystem.DeleteFile(file); - } - - public object Get(GetDefaultDirectoryBrowser request) => - ToOptimizedResult(new DefaultDirectoryBrowserInfo { Path = null }); - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetDirectoryContents request) - { - var path = request.Path; - - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(Path)); - } - - var networkPrefix = UncSeparatorString + UncSeparatorString; - - if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase) - && path.LastIndexOf(UncSeparator) == 1) - { - return ToOptimizedResult(Array.Empty()); - } - - return ToOptimizedResult(GetFileSystemEntries(request).ToList()); - } - - [Obsolete] - public object Get(GetNetworkShares request) - => ToOptimizedResult(Array.Empty()); - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetDrives request) - { - var result = GetDrives().ToList(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the list that is returned when an empty path is supplied - /// - /// IEnumerable{FileSystemEntryInfo}. - private IEnumerable GetDrives() - { - return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo - { - Name = d.Name, - Path = d.FullName, - Type = FileSystemEntryType.Directory - }); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetNetworkDevices request) - => ToOptimizedResult(Array.Empty()); - - /// - /// Gets the file system entries. - /// - /// The request. - /// IEnumerable{FileSystemEntryInfo}. - private IEnumerable GetFileSystemEntries(GetDirectoryContents request) - { - var entries = _fileSystem.GetFileSystemEntries(request.Path).OrderBy(i => i.FullName).Where(i => - { - var isDirectory = i.IsDirectory; - - if (!request.IncludeFiles && !isDirectory) - { - return false; - } - - return request.IncludeDirectories || !isDirectory; - }); - - return entries.Select(f => new FileSystemEntryInfo - { - Name = f.Name, - Path = f.FullName, - Type = f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File - - }); - } - - public object Get(GetParentPath request) - { - var parent = Path.GetDirectoryName(request.Path); - - if (string.IsNullOrEmpty(parent)) - { - // Check if unc share - var index = request.Path.LastIndexOf(UncSeparator); - - if (index != -1 && request.Path.IndexOf(UncSeparator) == 0) - { - parent = request.Path.Substring(0, index); - - if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator))) - { - parent = null; - } - } - } - - return parent; - } - } -} From c7fe8b04cc854df110528a0eda4383263e99e554 Mon Sep 17 00:00:00 2001 From: Bruce Date: Sat, 25 Apr 2020 19:59:31 +0100 Subject: [PATCH 078/724] 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 079/724] 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 080/724] 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 890e659cd390fc45c68b42c1a20f24a33e8c1570 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 25 Apr 2020 15:12:18 -0600 Subject: [PATCH 081/724] Fix autolaunch & redirect of swagger. --- Emby.Server.Implementations/Browser/BrowserLauncher.cs | 4 +++- Jellyfin.Server/Program.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 96096e142a..384cb049fa 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -1,5 +1,7 @@ using System; using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Browser @@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser /// The app host. public static void OpenSwaggerPage(IServerApplicationHost appHost) { - TryOpenUrl(appHost, "/swagger/index.html"); + TryOpenUrl(appHost, "/api-docs/v1/swagger"); } /// diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e55b0d4ed9..23ddcf159b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -529,7 +529,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (startupConfig != null && !startupConfig.HostWebClient()) { - inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/v1/swagger"; } return config From 000088f8f94e24ea715f15b722a2e64958bec07b Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 25 Apr 2020 18:18:33 -0600 Subject: [PATCH 082/724] init --- Jellyfin.Api/Controllers/LibraryController.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Jellyfin.Api/Controllers/LibraryController.cs diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs new file mode 100644 index 0000000000..f45101c0cb --- /dev/null +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -0,0 +1,56 @@ +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Library Controller. + /// + public class LibraryController : BaseJellyfinApiController + { + private readonly IProviderManager _providerManager; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + private readonly IAuthorizationContext _authContext; + private readonly IActivityManager _activityManager; + private readonly ILocalizationManager _localization; + private readonly ILibraryMonitor _libraryMonitor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public LibraryController( + IProviderManager providerManager, + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService, + IAuthorizationContext authContext, + IActivityManager activityManager, + ILocalizationManager localization, + ILibraryMonitor libraryMonitor) + { + _providerManager = providerManager; + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + _authContext = authContext; + _activityManager = activityManager; + _localization = localization; + _libraryMonitor = libraryMonitor; + } + } +} From 068368df6352cfad4e69df599c364b3f05b367ba Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 26 Apr 2020 23:28:32 -0600 Subject: [PATCH 083/724] Order actions by route, then http method --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 92bacb4400..00a73ade6f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -105,6 +105,10 @@ namespace Jellyfin.Server.Extensions { c.IncludeXmlComments(xmlFile); } + + // Order actions by route path, then by http method. + c.OrderActionsBy(description => + $"{description.ActionDescriptor.RouteValues["controller"]}_{description.HttpMethod}"); }); } } From c61a200c9de2714b3d6353f3a4ae52b8962d369a Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Tue, 28 Apr 2020 09:30:59 -0600 Subject: [PATCH 084/724] Revise documentation based on discussion in #2872 --- .../Controllers/NotificationsController.cs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 8da2a6c536..8feea9ab61 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -35,13 +35,14 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting a user's notifications. + /// Gets a user's notifications. /// /// The user's ID. /// An optional filter by notification read state. /// The optional index to start at. All notifications with a lower index will be omitted from the results. /// An optional limit on the number of notifications returned. - /// A read-only list of all of the user's notifications. + /// Notifications returned. + /// An containing a list of notifications. [HttpGet("{UserID}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNotifications( @@ -54,10 +55,11 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting a user's notification summary. + /// Gets a user's notification summary. /// /// The user's ID. - /// Notifications summary for the user. + /// Summary of user's notifications returned. + /// An containing a summary of the users notifications. [HttpGet("{UserID}/Summary")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNotificationsSummary( @@ -67,9 +69,10 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting notification types. + /// Gets notification types. /// - /// All notification types. + /// All notification types returned. + /// An containing a list of all notification types. [HttpGet("Types")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNotificationTypes() @@ -78,9 +81,10 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting notification services. + /// Gets notification services. /// - /// All notification services. + /// All notification services returned. + /// An containing a list of all notification services. [HttpGet("Services")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNotificationServices() @@ -89,13 +93,14 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint to send a notification to all admins. + /// Sends a notification to all admins. /// /// The name of the notification. /// The description of the notification. /// The URL of the notification. /// The level of the notification. - /// Status. + /// Notification sent. + /// An . [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CreateAdminNotification( @@ -120,11 +125,12 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint to set notifications as read. + /// Sets notifications as read. /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. - /// Status. + /// Notifications set as read. + /// An . [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetRead( @@ -135,11 +141,12 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint to set notifications as unread. + /// Sets notifications as unread. /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. - /// Status. + /// Notifications set as unread. + /// An . [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetUnread( From 806ae1bc07e715c6109a3e8ec96c6d3dd6a802ef Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 08:04:05 -0600 Subject: [PATCH 085/724] Remove versioned API --- .../Browser/BrowserLauncher.cs | 2 +- .../ApiApplicationBuilderExtensions.cs | 16 ++++++++-------- .../Extensions/ApiServiceCollectionExtensions.cs | 2 +- Jellyfin.Server/Program.cs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 384cb049fa..e706401fd1 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Browser /// The app host. public static void OpenSwaggerPage(IServerApplicationHost appHost) { - TryOpenUrl(appHost, "/api-docs/v1/swagger"); + TryOpenUrl(appHost, "/api-docs/swagger"); } /// diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 33fd77d9c7..745567703f 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Configuration; -using Jellyfin.Server.Middleware; using Microsoft.AspNetCore.Builder; namespace Jellyfin.Server.Extensions @@ -31,19 +30,20 @@ namespace Jellyfin.Server.Extensions return applicationBuilder .UseSwagger(c => { - c.RouteTemplate = $"/{baseUrl}api-docs/{{documentName}}/openapi.json"; + // Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs' + c.RouteTemplate = $"/{baseUrl}{{documentName}}/openapi.json"; }) .UseSwaggerUI(c => { - c.DocumentTitle = "Jellyfin API v1"; - c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1"); - c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger"; + c.DocumentTitle = "Jellyfin API"; + c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API"); + c.RoutePrefix = $"{baseUrl}api-docs/swagger"; }) .UseReDoc(c => { - c.DocumentTitle = "Jellyfin API v1"; - c.SpecUrl($"/{baseUrl}api-docs/v1/openapi.json"); - c.RoutePrefix = $"{baseUrl}api-docs/v1/redoc"; + c.DocumentTitle = "Jellyfin API"; + c.SpecUrl($"/{baseUrl}api-docs/openapi.json"); + c.RoutePrefix = $"{baseUrl}api-docs/redoc"; }); } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a24785d57e..a354f45aad 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -96,7 +96,7 @@ namespace Jellyfin.Server.Extensions { return serviceCollection.AddSwaggerGen(c => { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); + c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 23ddcf159b..7135800802 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -529,7 +529,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (startupConfig != null && !startupConfig.HostWebClient()) { - inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/v1/swagger"; + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger"; } return config From 97ecffceb7fe655010c1f415fd688b3ee0f9d48d Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 08:59:34 -0600 Subject: [PATCH 086/724] Add response code descriptions --- Jellyfin.Api/Controllers/StartupController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 14c59593fb..d60e46a01b 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -33,6 +33,7 @@ namespace Jellyfin.Api.Controllers /// /// Api endpoint for completing the startup wizard. /// + /// Startup wizard completed. /// Status. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -47,6 +48,7 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting the initial startup wizard configuration. /// + /// Initial startup wizard configuration retrieved. /// The initial startup wizard configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -68,6 +70,7 @@ namespace Jellyfin.Api.Controllers /// The UI language culture. /// The metadata country code. /// The preferred language for metadata. + /// Configuration saved. /// Status. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -88,6 +91,7 @@ namespace Jellyfin.Api.Controllers /// /// Enable remote access. /// Enable UPnP. + /// Configuration saved. /// Status. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -102,6 +106,7 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for returning the first user. /// + /// Initial user retrieved. /// The first user. [HttpGet("User")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -115,6 +120,7 @@ namespace Jellyfin.Api.Controllers /// Endpoint for updating the user name and password. /// /// The DTO containing username and password. + /// Updated user name and password. /// The async task. [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] From 7a3925b863a12bea492a93f41cda4eb92dc9c183 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 09:41:12 -0600 Subject: [PATCH 087/724] Fix docs --- Jellyfin.Api/Controllers/StartupController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index d60e46a01b..66e4774aa0 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Api endpoint for completing the startup wizard. + /// Completes the startup wizard. /// /// Startup wizard completed. /// Status. @@ -46,7 +46,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting the initial startup wizard configuration. + /// Gets the initial startup wizard configuration. /// /// Initial startup wizard configuration retrieved. /// The initial startup wizard configuration. @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for updating the initial startup wizard configuration. + /// Sets the initial startup wizard configuration. /// /// The UI language culture. /// The metadata country code. @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for (dis)allowing remote access and UPnP. + /// Sets remote access and UPnP. /// /// Enable remote access. /// Enable UPnP. @@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for returning the first user. + /// Gets the first user. /// /// Initial user retrieved. /// The first user. @@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for updating the user name and password. + /// Sets the user name and password. /// /// The DTO containing username and password. /// Updated user name and password. From 82231b4393bb367f7fca50fed21f00e469b9f960 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 29 Apr 2020 15:53:29 -0600 Subject: [PATCH 088/724] Update to return IEnumerable directly where possible --- Jellyfin.Api/Controllers/NotificationsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 8feea9ab61..3cbb3a3a3f 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers /// An containing a list of all notification types. [HttpGet("Types")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetNotificationTypes() + public IEnumerable GetNotificationTypes() { return _notificationManager.GetNotificationTypes(); } @@ -87,9 +87,9 @@ namespace Jellyfin.Api.Controllers /// An containing a list of all notification services. [HttpGet("Services")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetNotificationServices() + public IEnumerable GetNotificationServices() { - return _notificationManager.GetNotificationServices().ToList(); + return _notificationManager.GetNotificationServices(); } /// From 0d8253d8e22d4cf34c58577e7fefb3f5733adedd Mon Sep 17 00:00:00 2001 From: Bruce Date: Fri, 1 May 2020 15:17:40 +0100 Subject: [PATCH 089/724] 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 0017163f39438e2718f7c95b3fb65df5dde65e3d Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:06:29 -0600 Subject: [PATCH 090/724] Update endpoint docs --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0d375e668a..2837ea8e87 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -34,7 +34,9 @@ namespace Jellyfin.Api.Controllers /// Display preferences id. /// User id. /// Client. - /// Display Preferences. + /// Display preferences retrieved. + /// Specified display preferences not found. + /// An containing the display preferences on success, or a if the display preferences could not be found. [HttpGet("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -59,7 +61,9 @@ namespace Jellyfin.Api.Controllers /// User Id. /// Client. /// New Display Preferences object. - /// Status. + /// Display preferences updated. + /// Specified display preferences not found. + /// An on success, or a if the display preferences could not be found. [HttpPost("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] From f67daa84b04ae6c8ffcc42c038a65ecb8a433861 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:10:59 -0600 Subject: [PATCH 091/724] Update endpoint docs --- .../Controllers/ScheduledTasksController.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index da7cfbc3a7..ad70bf83b2 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -33,7 +33,8 @@ namespace Jellyfin.Api.Controllers /// /// Optional filter tasks that are hidden, or not. /// Optional filter tasks that are enabled, or not. - /// Task list. + /// Scheduled tasks retrieved. + /// The list of scheduled tasks. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetTasks( @@ -65,7 +66,9 @@ namespace Jellyfin.Api.Controllers /// Get task by id. /// /// Task Id. - /// Task Info. + /// Task retrieved. + /// Task not found. + /// An containing the task on success, or a if the task could not be found. [HttpGet("{TaskID}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -87,7 +90,9 @@ namespace Jellyfin.Api.Controllers /// Start specified task. /// /// Task Id. - /// Status. + /// Task started. + /// Task not found. + /// An on success, or a if the file could not be found. [HttpPost("Running/{TaskID}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -109,7 +114,9 @@ namespace Jellyfin.Api.Controllers /// Stop specified task. /// /// Task Id. - /// Status. + /// Task stopped. + /// Task not found. + /// An on success, or a if the file could not be found. [HttpDelete("Running/{TaskID}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -132,7 +139,9 @@ namespace Jellyfin.Api.Controllers /// /// Task Id. /// Triggers. - /// Status. + /// Task triggers updated. + /// Task not found. + /// An on success, or a if the file could not be found. [HttpPost("{TaskID}/Triggers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] From 7516e3ebbec82b732e8e4355ae108e7030e1e00e Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:12:56 -0600 Subject: [PATCH 092/724] 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 093/724] 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 cbd4a64e670eda1c30be6000a8f6cceccc93ddfa Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 18:46:27 -0600 Subject: [PATCH 094/724] Update endpoint docs --- .../Images/ImageByNameController.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index ce509b4e6d..6160d54028 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -42,10 +42,11 @@ namespace Jellyfin.Api.Controllers.Images /// /// Get all general images. /// - /// General images. + /// Retrieved list of images. + /// An containing the list of images. [HttpGet("General")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetGeneralImages() + public ActionResult> GetGeneralImages() { return Ok(GetImageList(_applicationPaths.GeneralPath, false)); } @@ -55,7 +56,9 @@ namespace Jellyfin.Api.Controllers.Images /// /// The name of the image. /// Image Type (primary, backdrop, logo, etc). - /// Image Stream. + /// Image stream retrieved. + /// Image not found. + /// A containing the image contents on success, or a if the image could not be found. [HttpGet("General/{Name}/{Type}")] [Produces("application/octet-stream")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -82,10 +85,11 @@ namespace Jellyfin.Api.Controllers.Images /// /// Get all general images. /// - /// General images. + /// Retrieved list of images. + /// An containing the list of images. [HttpGet("Ratings")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetRatingImages() + public ActionResult> GetRatingImages() { return Ok(GetImageList(_applicationPaths.RatingsPath, false)); } @@ -95,7 +99,9 @@ namespace Jellyfin.Api.Controllers.Images /// /// The theme to get the image from. /// The name of the image. - /// Image Stream. + /// Image stream retrieved. + /// Image not found. + /// A containing the image contents on success, or a if the image could not be found. [HttpGet("Ratings/{Theme}/{Name}")] [Produces("application/octet-stream")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -110,7 +116,8 @@ namespace Jellyfin.Api.Controllers.Images /// /// Get all media info images. /// - /// Media Info images. + /// Image list retrieved. + /// An containing the list of images. [HttpGet("MediaInfo")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetMediaInfoImages() @@ -123,7 +130,9 @@ namespace Jellyfin.Api.Controllers.Images /// /// The theme to get the image from. /// The name of the image. - /// Image Stream. + /// Image stream retrieved. + /// Image not found. + /// A containing the image contents on success, or a if the image could not be found. [HttpGet("MediaInfo/{Theme}/{Name}")] [Produces("application/octet-stream")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -141,7 +150,7 @@ namespace Jellyfin.Api.Controllers.Images /// Path to begin search. /// Theme to search. /// File name to search for. - /// Image Stream. + /// A containing the image contents on success, or a if the image could not be found. private ActionResult GetImageFile(string basePath, string theme, string name) { var themeFolder = Path.Combine(basePath, theme); From 35dbcea9311589ea7b9a10ab02da557a2bfb46fc Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 18:47:05 -0600 Subject: [PATCH 095/724] Return array -> ienumerable --- Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index 6160d54028..67ebaa4e09 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers.Images /// An containing the list of images. [HttpGet("MediaInfo")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetMediaInfoImages() + public ActionResult> GetMediaInfoImages() { return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false)); } From d7d8118b42c8abc8a4f12c4f2b0fb97cc6384ba7 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 3 May 2020 14:02:15 -0600 Subject: [PATCH 096/724] Fix xml docs --- Jellyfin.Api/Controllers/NotificationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 3cbb3a3a3f..8d82ca10f1 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets notification services. /// - /// All notification services returned. + /// All notification services returned. /// An containing a list of all notification services. [HttpGet("Services")] [ProducesResponseType(StatusCodes.Status200OK)] From 2b1b9a64b6b7a3bac4d96642cda7a0c55d5cae74 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 8 May 2020 08:40:37 -0600 Subject: [PATCH 097/724] Add OperationId to SwaggerGen --- .../Extensions/ApiServiceCollectionExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a354f45aad..344ef6a5ff 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ 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; @@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Extensions { @@ -112,6 +114,10 @@ namespace Jellyfin.Server.Extensions // Order actions by route path, then by http method. c.OrderActionsBy(description => $"{description.ActionDescriptor.RouteValues["controller"]}_{description.HttpMethod}"); + + // Use method name as operationId + c.CustomOperationIds(description => + description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); }); } } From 37f55b5c217db5343ab196094f67dc84e71d4ef0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 17 May 2020 19:56:02 -0600 Subject: [PATCH 098/724] apply doc suggestions --- Jellyfin.Api/Controllers/StartupController.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 66e4774aa0..ed1dc1ede3 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -34,7 +34,7 @@ namespace Jellyfin.Api.Controllers /// Completes the startup wizard. /// /// Startup wizard completed. - /// Status. + /// An indicating success. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CompleteWizard() @@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers /// Gets the initial startup wizard configuration. /// /// Initial startup wizard configuration retrieved. - /// The initial startup wizard configuration. + /// An containing the initial startup wizard configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetStartupConfiguration() @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers /// The metadata country code. /// The preferred language for metadata. /// Configuration saved. - /// Status. + /// An indicating success. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateInitialConfiguration( @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// Enable remote access. /// Enable UPnP. /// Configuration saved. - /// Status. + /// An indicating success. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) @@ -121,7 +121,10 @@ namespace Jellyfin.Api.Controllers /// /// The DTO containing username and password. /// Updated user name and password. - /// The async task. + /// + /// A that represents the asynchronous update operation. + /// The task result contains an indicating success. + /// [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) From c4f8ba55f2b3424be4a6ff1044d13327fe36b687 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 08:28:02 -0600 Subject: [PATCH 099/724] 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 100/724] 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 101/724] 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 102/724] 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 103/724] 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 104/724] 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 105/724] 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 106/724] 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 1c471d58551043dab3c808952d9834163cac3078 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:01:00 -0600 Subject: [PATCH 107/724] Clean UpdateDisplayPreferences endpoint --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 2837ea8e87..579b5df5d4 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -74,14 +74,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string client, [FromBody, BindRequired] DisplayPreferences displayPreferences) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - if (displayPreferencesId == null) { - // do nothing. + // TODO - refactor so parameter doesn't exist or is actually used. } _displayPreferencesRepository.SaveDisplayPreferences( From c998935d29d04a55babdeb0adcf1d1091611b1e3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:06:37 -0600 Subject: [PATCH 108/724] Apply review suggestions --- Jellyfin.Api/Controllers/ScheduledTasksController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index ad70bf83b2..3e3359ec77 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; -using MediaBrowser.Controller.Net; +using Jellyfin.Api.Constants; using MediaBrowser.Model.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -14,7 +15,7 @@ namespace Jellyfin.Api.Controllers /// /// Scheduled Tasks Controller. /// - // [Authenticated] + [Authorize(Policy = Policies.RequiresElevation)] public class ScheduledTasksController : BaseJellyfinApiController { private readonly ITaskManager _taskManager; @@ -82,8 +83,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var result = ScheduledTaskHelpers.GetTaskInfo(task); - return Ok(result); + return ScheduledTaskHelpers.GetTaskInfo(task); } /// From 98bd61e36443452a280dc9d3543baecc10b561ed Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:14:37 -0600 Subject: [PATCH 109/724] Clean up routes --- Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index 67ebaa4e09..62fcb5a2a6 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetGeneralImages() { - return Ok(GetImageList(_applicationPaths.GeneralPath, false)); + return GetImageList(_applicationPaths.GeneralPath, false); } /// @@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRatingImages() { - return Ok(GetImageList(_applicationPaths.RatingsPath, false)); + return GetImageList(_applicationPaths.RatingsPath, false); } /// @@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers.Images [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetMediaInfoImages() { - return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false)); + return GetImageList(_applicationPaths.MediaInfoImagesPath, false); } /// From 2923013c6ed0c5c4e7325893be0822d8fcd9de47 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:23:28 -0600 Subject: [PATCH 110/724] Clean Remote Image Controller. --- .../Images/RemoteImageController.cs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs index a0754ed4eb..665db561bf 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -9,12 +10,12 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers.Images /// Remote Images Controller. /// [Route("Images")] - [Authenticated] + [Authorize] public class RemoteImageController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; @@ -60,7 +61,9 @@ namespace Jellyfin.Api.Controllers.Images /// 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 image provider to use. - /// Optinal. Include all languages. + /// Optional. Include all languages. + /// Remote Images returned. + /// Item not found. /// Remote Image Result. [HttpGet("{Id}/RemoteImages")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -116,18 +119,20 @@ namespace Jellyfin.Api.Controllers.Images } result.Images = imageArray; - return Ok(result); + return result; } /// /// Gets available remote image providers for an item. /// /// Item Id. - /// List of providers. + /// Returned remote image providers. + /// Item not found. + /// List of remote image providers. [HttpGet("{Id}/RemoteImages/Providers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetRemoteImageProviders([FromRoute] string id) + public ActionResult> GetRemoteImageProviders([FromRoute] string id) { var item = _libraryManager.GetItemById(id); if (item == null) @@ -135,14 +140,15 @@ namespace Jellyfin.Api.Controllers.Images return NotFound(); } - var providers = _providerManager.GetRemoteImageProviderInfo(item); - return Ok(providers); + return Ok(_providerManager.GetRemoteImageProviderInfo(item)); } /// /// Gets a remote image. /// /// The image url. + /// Remote image returned. + /// Remote image not found. /// Image Stream. [HttpGet("Remote")] [Produces("application/octet-stream")] @@ -154,7 +160,7 @@ namespace Jellyfin.Api.Controllers.Images var pointerCachePath = GetFullCachePath(urlHash.ToString()); string? contentPath = null; - bool hasFile = false; + var hasFile = false; try { @@ -166,11 +172,11 @@ namespace Jellyfin.Api.Controllers.Images } catch (FileNotFoundException) { - // Means the file isn't cached yet + // The file isn't cached yet } catch (IOException) { - // Means the file isn't cached yet + // The file isn't cached yet } if (!hasFile) @@ -194,7 +200,9 @@ namespace Jellyfin.Api.Controllers.Images /// Item Id. /// The image type. /// The image url. - /// Status. + /// Remote image downloaded. + /// Remote image not found. + /// Download status. [HttpPost("{Id}/RemoteImages/Download")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -245,10 +253,10 @@ namespace Jellyfin.Api.Controllers.Images var fullCachePath = GetFullCachePath(urlHash + "." + ext); Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) + await using (var stream = result.Content) { - using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); - await stream.CopyToAsync(filestream).ConfigureAwait(false); + await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await stream.CopyToAsync(fileStream).ConfigureAwait(false); } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); From 839de72f9a320bfe09cdd9c2fcab6806f3106916 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:24:04 -0600 Subject: [PATCH 111/724] Fix authentication attribute --- Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index 62fcb5a2a6..dadb344385 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -7,10 +7,10 @@ using System.Linq; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers.Images /// Images By Name Controller. /// [Route("Images")] - [Authenticated] + [Authorize] public class ImageByNameController : BaseJellyfinApiController { private readonly IServerApplicationPaths _applicationPaths; From 6dbbfcbfbef28e8866aa0144170e1edfff1a2bcb Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:29:59 -0600 Subject: [PATCH 112/724] update xml docs --- Jellyfin.Api/Controllers/ConfigurationController.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index b508ac0547..992cb00874 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -44,6 +44,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets application configuration. /// + /// Application configuration returned. /// Application configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -56,7 +57,8 @@ namespace Jellyfin.Api.Controllers /// Updates application configuration. /// /// Configuration. - /// Status. + /// Configuration updated. + /// Update status. [HttpPost("Configuration")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -70,6 +72,7 @@ namespace Jellyfin.Api.Controllers /// Gets a named configuration. /// /// Configuration key. + /// Configuration returned. /// Configuration. [HttpGet("Configuration/{Key}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -82,7 +85,8 @@ namespace Jellyfin.Api.Controllers /// Updates named configuration. /// /// Configuration key. - /// Status. + /// Named configuration updated. + /// Update status. [HttpPost("Configuration/{Key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -103,7 +107,8 @@ namespace Jellyfin.Api.Controllers /// /// Gets a default MetadataOptions object. /// - /// MetadataOptions. + /// Metadata options returned. + /// Default MetadataOptions. [HttpGet("Configuration/MetadataOptions/Default")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -116,6 +121,7 @@ namespace Jellyfin.Api.Controllers /// Updates the path to the media encoder. /// /// Media encoder path form body. + /// Media encoder path updated. /// Status. [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] From 6c376f18f7f094d41a0d3e5387ce83e5b0b66c4a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:47:02 -0600 Subject: [PATCH 113/724] update xml docs --- Jellyfin.Api/Controllers/ChannelsController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 4e2621b7b3..733f1e6d86 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -21,6 +22,7 @@ namespace Jellyfin.Api.Controllers /// /// Channels Controller. /// + [Authorize] public class ChannelsController : BaseJellyfinApiController { private readonly IChannelManager _channelManager; @@ -46,6 +48,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Filter by channels that support getting latest items. /// Optional. Filter by channels that support media deletion. /// Optional. Filter by channels that are favorite. + /// Channels returned. /// Channels. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] @@ -71,6 +74,7 @@ namespace Jellyfin.Api.Controllers /// /// Get all channel features. /// + /// All channel features returned. /// Channel features. [HttpGet("Features")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -83,6 +87,7 @@ namespace Jellyfin.Api.Controllers /// Get channel features. /// /// Channel id. + /// Channel features returned. /// Channel features. [HttpGet("{Id}/Features")] public ActionResult GetChannelFeatures([FromRoute] string id) @@ -102,6 +107,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Channel items returned. /// Channel items. [HttpGet("{Id}/Items")] public async Task>> GetChannelItems( @@ -175,6 +181,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. Specify one or more channel id's, comma delimited. + /// Latest channel items returned. /// Latest channel items. public async Task>> GetLatestChannelItems( [FromQuery] Guid? userId, From e03c97d7cdfad65a48bc0aff6ca0e45f9b3ec3cd Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:02:52 -0600 Subject: [PATCH 114/724] update xml docs --- Jellyfin.Api/Controllers/EnvironmentController.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 139c1af083..78c206ba19 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -40,7 +40,8 @@ namespace Jellyfin.Api.Controllers /// The path. /// An optional filter to include or exclude files from the results. true/false. /// An optional filter to include or exclude folders from the results. true/false. - /// File system entries. + /// Directory contents returned. + /// Directory contents. [HttpGet("DirectoryContents")] [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetDirectoryContents( @@ -79,7 +80,9 @@ namespace Jellyfin.Api.Controllers /// Validates path. /// /// Validate request object. - /// Status. + /// Path validated. + /// Path not found. + /// Validation status. [HttpPost("ValidatePath")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -132,6 +135,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets network paths. /// + /// Empty array returned. /// List of entries. [Obsolete("This endpoint is obsolete.")] [HttpGet("NetworkShares")] @@ -144,6 +148,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets available drives from the server's file system. /// + /// List of entries returned. /// List of entries. [HttpGet("Drives")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -189,6 +194,7 @@ namespace Jellyfin.Api.Controllers /// /// Get Default directory browser. /// + /// Default directory browser returned. /// Default directory browser. [HttpGet("DefaultDirectoryBrowser")] [ProducesResponseType(StatusCodes.Status200OK)] From a11a1934399b8cbce0487ced49d2f8e7065b436a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:04:09 -0600 Subject: [PATCH 115/724] 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 116/724] 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 117/724] 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 118/724] 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 2f2bceb1104d8ea669ca21fc40200247aca956ed Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 12:56:57 -0600 Subject: [PATCH 119/724] Remove default parameter values --- Jellyfin.Api/Controllers/ScheduledTasksController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 3e3359ec77..19cce974ea 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -39,8 +39,8 @@ namespace Jellyfin.Api.Controllers [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetTasks( - [FromQuery] bool? isHidden = false, - [FromQuery] bool? isEnabled = false) + [FromQuery] bool? isHidden, + [FromQuery] bool? isEnabled) { IEnumerable tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); From b28dd47a0fc5b18111678acede335474f9007b8f Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 12:58:09 -0600 Subject: [PATCH 120/724] 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 121/724] 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 122/724] 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 070edd9e5bff63d3f158b6ca8b37095adc686492 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 13:13:07 -0600 Subject: [PATCH 123/724] Fix MediaType usage --- Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index dadb344385..fa60809773 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Mime; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -60,7 +61,7 @@ namespace Jellyfin.Api.Controllers.Images /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("General/{Name}/{Type}")] - [Produces("application/octet-stream")] + [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type) @@ -103,7 +104,7 @@ namespace Jellyfin.Api.Controllers.Images /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("Ratings/{Theme}/{Name}")] - [Produces("application/octet-stream")] + [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetRatingImage( @@ -134,7 +135,7 @@ namespace Jellyfin.Api.Controllers.Images /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("MediaInfo/{Theme}/{Name}")] - [Produces("application/octet-stream")] + [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetMediaInfoImage( From 5f0c37d5745cbf2632d377905a0763f0254bca08 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 13:22:09 -0600 Subject: [PATCH 124/724] Fix DefaultDirectoryBrowserInfo naming --- Jellyfin.Api/Controllers/EnvironmentController.cs | 4 ++-- ...ectoryBrowserInfo.cs => DefaultDirectoryBrowserInfoDto.cs} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename Jellyfin.Api/Models/EnvironmentDtos/{DefaultDirectoryBrowserInfo.cs => DefaultDirectoryBrowserInfoDto.cs} (84%) diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 78c206ba19..8d9d2642f4 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -198,9 +198,9 @@ namespace Jellyfin.Api.Controllers /// Default directory browser. [HttpGet("DefaultDirectoryBrowser")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetDefaultDirectoryBrowser() + public ActionResult GetDefaultDirectoryBrowser() { - return new DefaultDirectoryBrowserInfo(); + return new DefaultDirectoryBrowserInfoDto(); } } } diff --git a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs similarity index 84% rename from Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs rename to Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs index 6b1c750bf6..a86815b81c 100644 --- a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs +++ b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs @@ -3,7 +3,7 @@ namespace Jellyfin.Api.Models.EnvironmentDtos /// /// Default directory browser info. /// - public class DefaultDirectoryBrowserInfo + public class DefaultDirectoryBrowserInfoDto { /// /// Gets or sets the path. From fb068b76a12bf9de5de75a0b8079effcd0336ecf Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 20 May 2020 07:18:51 -0600 Subject: [PATCH 125/724] Use correct MediaTypeName --- Jellyfin.Api/Controllers/Images/RemoteImageController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs index 665db561bf..1155cc653e 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -151,7 +152,7 @@ namespace Jellyfin.Api.Controllers.Images /// Remote image not found. /// Image Stream. [HttpGet("Remote")] - [Produces("application/octet-stream")] + [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetRemoteImage([FromQuery, BindRequired] string imageUrl) From 341b947cdecdfc791c1bc3e72da1e68cd3754c3a Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 22 May 2020 10:48:01 -0600 Subject: [PATCH 126/724] Move int64 converter to JsonDefaults location --- .../Extensions/ApiServiceCollectionExtensions.cs | 2 -- .../Json/Converters/JsonInt64Converter.cs | 10 +++++----- MediaBrowser.Common/Json/JsonDefaults.cs | 1 + 3 files changed, 6 insertions(+), 7 deletions(-) rename Jellyfin.Server/Converters/LongToStringConverter.cs => MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs (85%) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index afd42ac5ac..71ef9a69a2 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; -using Jellyfin.Server.Converters; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -76,7 +75,6 @@ namespace Jellyfin.Server.Extensions { // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. options.JsonSerializerOptions.PropertyNamingPolicy = null; - options.JsonSerializerOptions.Converters.Add(new LongToStringConverter()); }) .AddControllersAsServices(); } diff --git a/Jellyfin.Server/Converters/LongToStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs similarity index 85% rename from Jellyfin.Server/Converters/LongToStringConverter.cs rename to MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs index ad66b7b0c3..d18fd95d5f 100644 --- a/Jellyfin.Server/Converters/LongToStringConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs @@ -5,16 +5,16 @@ using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -namespace Jellyfin.Server.Converters +namespace MediaBrowser.Common.Json.Converters { /// /// Long to String JSON converter. /// Javascript does not support 64-bit integers. /// - public class LongToStringConverter : JsonConverter + public class JsonInt64Converter : JsonConverter { /// - /// Read JSON string as Long. + /// Read JSON string as int64. /// /// . /// Type. @@ -25,8 +25,8 @@ namespace Jellyfin.Server.Converters if (reader.TokenType == JsonTokenType.String) { // try to parse number directly from bytes - ReadOnlySpan span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) && span.Length == bytesConsumed) + var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (Utf8Parser.TryParse(span, out long number, out var bytesConsumed) && span.Length == bytesConsumed) { return number; } diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 4a6ee0a793..a7f5fde050 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -23,6 +23,7 @@ namespace MediaBrowser.Common.Json options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonInt64Converter()); return options; } From a4b3f2e32b68a61407876ab11343936b14cc1191 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 23 May 2020 18:19:49 -0600 Subject: [PATCH 127/724] Add missing route attribute --- Jellyfin.Api/Controllers/ChannelsController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 733f1e6d86..8e0f766978 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -183,6 +183,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify one or more channel id's, comma delimited. /// Latest channel items returned. /// Latest channel items. + [HttpGet("Items/Latest")] public async Task>> GetLatestChannelItems( [FromQuery] Guid? userId, [FromQuery] int? startIndex, From 70c42eb0acd0e6572f3ca9313716a8dd71b247ee Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 24 May 2020 12:19:26 -0600 Subject: [PATCH 128/724] Apply review suggestions --- .../Controllers/ChannelsController.cs | 33 +++++++++++-------- .../RequestHelpers.cs} | 0 2 files changed, 19 insertions(+), 14 deletions(-) rename Jellyfin.Api/{Extensions/RequestExtensions.cs => Helpers/RequestHelpers.cs} (100%) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 8e0f766978..7c055874d7 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -42,14 +42,14 @@ namespace Jellyfin.Api.Controllers /// /// Gets available channels. /// - /// User Id. + /// User Id to filter by. Use to not filter by user. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. /// Optional. Filter by channels that support getting latest items. /// Optional. Filter by channels that support media deletion. /// Optional. Filter by channels that are favorite. /// Channels returned. - /// Channels. + /// An containing the channels. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetChannels( @@ -75,10 +75,10 @@ namespace Jellyfin.Api.Controllers /// Get all channel features. /// /// All channel features returned. - /// Channel features. + /// An containing the channel features. [HttpGet("Features")] [ProducesResponseType(StatusCodes.Status200OK)] - public IEnumerable GetAllChannelFeatures() + public ActionResult> GetAllChannelFeatures() { return _channelManager.GetAllChannelFeatures(); } @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers /// /// Channel id. /// Channel features returned. - /// Channel features. + /// An containing the channel features. [HttpGet("{Id}/Features")] public ActionResult GetChannelFeatures([FromRoute] string id) { @@ -99,11 +99,11 @@ namespace Jellyfin.Api.Controllers /// Get channel items. /// /// Channel Id. - /// Folder Id. - /// User Id. + /// Optional. Folder Id. + /// Optional. User Id. /// 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. - /// Sort Order - Ascending,Descending. + /// Optional. Sort Order - Ascending,Descending. /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. @@ -175,14 +175,17 @@ namespace Jellyfin.Api.Controllers /// /// Gets latest channel items. /// - /// User Id. + /// Optional. User Id. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. Specify one or more channel id's, comma delimited. /// Latest channel items returned. - /// Latest channel items. + /// + /// A representing the request to get the latest channel items. + /// The task result contains an containing the latest channel items. + /// [HttpGet("Items/Latest")] public async Task>> GetLatestChannelItems( [FromQuery] Guid? userId, @@ -192,7 +195,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string fields, [FromQuery] string channelIds) { - var user = userId == null + var user = userId == null || userId == Guid.Empty ? null : _userManager.GetUserById(userId.Value); @@ -200,9 +203,11 @@ namespace Jellyfin.Api.Controllers { Limit = limit, StartIndex = startIndex, - ChannelIds = - (channelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(i => new Guid(i)).ToArray(), + ChannelIds = (channelIds ?? string.Empty) + .Split(',') + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => new Guid(i)) + .ToArray(), DtoOptions = new DtoOptions { Fields = RequestExtensions.GetItemFields(fields) } }; diff --git a/Jellyfin.Api/Extensions/RequestExtensions.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs similarity index 100% rename from Jellyfin.Api/Extensions/RequestExtensions.cs rename to Jellyfin.Api/Helpers/RequestHelpers.cs From 1f9cda6a6600a81969cf8afad3c60fa77bb5a093 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 24 May 2020 12:19:37 -0600 Subject: [PATCH 129/724] Add ImageTags to SwaggerGenTypes --- .../ApiServiceCollectionExtensions.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 71ef9a69a2..4d00c513ba 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,13 +1,17 @@ +using System.Collections.Generic; +using System.Linq; using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Extensions { @@ -89,7 +93,25 @@ namespace Jellyfin.Server.Extensions return serviceCollection.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); + c.MapSwaggerGenTypes(); }); } + + private static void MapSwaggerGenTypes(this SwaggerGenOptions options) + { + // BaseItemDto.ImageTags + options.MapType>(() => + new OpenApiSchema + { + Type = "object", + Properties = typeof(ImageType).GetEnumNames().ToDictionary( + name => name, + name => new OpenApiSchema + { + Type = "string", + Format = "string" + }) + }); + } } } From 40762f13c6d4ebe0baef2cc241dfbb2a908999b4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 24 May 2020 15:54:34 -0600 Subject: [PATCH 130/724] Fix route parameter casing --- Jellyfin.Api/Controllers/ChannelsController.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index f35f822019..e25b4c8219 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -86,19 +86,19 @@ namespace Jellyfin.Api.Controllers /// /// Get channel features. /// - /// Channel id. + /// Channel id. /// Channel features returned. /// An containing the channel features. - [HttpGet("{Id}/Features")] - public ActionResult GetChannelFeatures([FromRoute] string id) + [HttpGet("{channelId}/Features")] + public ActionResult GetChannelFeatures([FromRoute] string channelId) { - return _channelManager.GetChannelFeatures(id); + return _channelManager.GetChannelFeatures(channelId); } /// /// Get channel items. /// - /// Channel Id. + /// Channel Id. /// Optional. Folder Id. /// Optional. User Id. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. @@ -109,9 +109,9 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Channel items returned. /// Channel items. - [HttpGet("{Id}/Items")] + [HttpGet("{channelId}/Items")] public async Task>> GetChannelItems( - [FromRoute] Guid id, + [FromRoute] Guid channelId, [FromQuery] Guid? folderId, [FromQuery] Guid? userId, [FromQuery] int? startIndex, @@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers { Limit = limit, StartIndex = startIndex, - ChannelIds = new[] { id }, + ChannelIds = new[] { channelId }, ParentId = folderId ?? Guid.Empty, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), DtoOptions = new DtoOptions { Fields = RequestHelpers.GetItemFields(fields) } From 483e24607b92b2989158670ba4f36da0361d52e2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 24 May 2020 16:01:53 -0600 Subject: [PATCH 131/724] Fix optional parameter binding --- Jellyfin.Api/Controllers/ChannelsController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index e25b4c8219..6b42b500e0 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -116,10 +116,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery] string sortOrder, - [FromQuery] string filters, - [FromQuery] string sortBy, - [FromQuery] string fields) + [FromQuery] string? sortOrder, + [FromQuery] string? filters, + [FromQuery] string? sortBy, + [FromQuery] string? fields) { var user = userId == null ? null From e02cc8da53ff76a17de52a18ad83e73a1caa6394 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 24 May 2020 16:04:47 -0600 Subject: [PATCH 132/724] Add Swashbuckle TODO note --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 4d08cddbc0..7bb659ecee 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -120,6 +120,8 @@ namespace Jellyfin.Server.Extensions description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); // Add types not supported by System.Text.Json + // TODO: Remove this once these types are supported by System.Text.Json and Swashbuckle + // See: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1667 c.MapSwaggerGenTypes(); }); } From a5a39300bc733ad7b1d3c683f5f290a742171661 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 26 May 2020 16:55:27 +0200 Subject: [PATCH 133/724] 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 36312c92f56671484caaeaf89e28f7737723e97d Mon Sep 17 00:00:00 2001 From: Ken Brazier Date: Sun, 31 May 2020 16:40:02 -0600 Subject: [PATCH 134/724] 2354 open soft-links to read size --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 7461ec4f1d..8b75e8c708 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO if (info is FileInfo fileInfo) { result.Length = fileInfo.Length; + + // Issue #2354 get the size of files behind symbolic links + if(fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + using (Stream thisFileStream = File.OpenRead(fileInfo.ToString())) + { + result.Length = thisFileStream.Length; + } + } + result.DirectoryName = fileInfo.DirectoryName; } From b944b8f8c54963f61eee5eeb97cd1745ae42ac50 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 11:03:08 -0600 Subject: [PATCH 135/724] 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 136/724] 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 dfe873fc293cf940a4f3d25aacdc8dfc53f150dc Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 11:12:33 -0600 Subject: [PATCH 137/724] Add Authentication to openapi generation. --- .../ApiServiceCollectionExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 344ef6a5ff..a6817421a8 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -99,6 +99,25 @@ namespace Jellyfin.Server.Extensions return serviceCollection.AddSwaggerGen(c => { c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" }); + c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Name = "X-Emby-Token", + Description = "API key header parameter" + }); + + var securitySchemeRef = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = AuthenticationSchemes.CustomAuthentication }, + }; + + // TODO: Apply this with an operation filter instead of globally + // https://github.com/domaindrivendev/Swashbuckle.AspNetCore#add-security-definitions-and-requirements + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { securitySchemeRef, Array.Empty() } + }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( From e30a85025f3d0f8b936827613239da7c2c2387c2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 12:42:59 -0600 Subject: [PATCH 138/724] 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 aed6f57f11e4d08372fcf456742bdaedea374f6d Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 20:54:02 -0600 Subject: [PATCH 139/724] Remove invalid docs and null check --- .../Controllers/DisplayPreferencesController.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 579b5df5d4..35efe6b5f8 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -35,7 +35,6 @@ namespace Jellyfin.Api.Controllers /// User id. /// Client. /// Display preferences retrieved. - /// Specified display preferences not found. /// An containing the display preferences on success, or a if the display preferences could not be found. [HttpGet("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -45,13 +44,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] [Required] string userId, [FromQuery] [Required] string client) { - var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); - if (result == null) - { - return NotFound(); - } - - return result; + return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); } /// @@ -62,7 +55,6 @@ namespace Jellyfin.Api.Controllers /// Client. /// New Display Preferences object. /// Display preferences updated. - /// Specified display preferences not found. /// An on success, or a if the display preferences could not be found. [HttpPost("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] From 6d9f564a949e326e909cbcfd37d254195b40ba56 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 2 Jun 2020 15:04:55 +0200 Subject: [PATCH 140/724] 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 141/724] 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 142/724] 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 143/724] 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 144/724] 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 145/724] 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 146/724] 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 147/724] 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 148/724] 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 22f56842bd6422a8f2789a2ce5a7d6f4caf563f2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 4 Jun 2020 07:59:11 -0600 Subject: [PATCH 149/724] Apply review suggestions --- .../Controllers/EnvironmentController.cs | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 8d9d2642f4..35cd89e0e8 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -21,17 +22,20 @@ namespace Jellyfin.Api.Controllers public class EnvironmentController : BaseJellyfinApiController { private const char UncSeparator = '\\'; - private const string UncSeparatorString = "\\"; + private const string UncStartPrefix = @"\\"; private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - public EnvironmentController(IFileSystem fileSystem) + /// Instance of the interface. + public EnvironmentController(IFileSystem fileSystem, ILogger logger) { _fileSystem = fileSystem; + _logger = logger; } /// @@ -46,27 +50,19 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetDirectoryContents( [FromQuery, BindRequired] string path, - [FromQuery] bool includeFiles, - [FromQuery] bool includeDirectories) + [FromQuery] bool includeFiles = false, + [FromQuery] bool includeDirectories = false) { - const string networkPrefix = UncSeparatorString + UncSeparatorString; - if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase) + if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase) && path.LastIndexOf(UncSeparator) == 1) { return Array.Empty(); } - var entries = _fileSystem.GetFileSystemEntries(path).OrderBy(i => i.FullName).Where(i => - { - var isDirectory = i.IsDirectory; - - if (!includeFiles && !isDirectory) - { - return false; - } - - return includeDirectories || !isDirectory; - }); + var entries = + _fileSystem.GetFileSystemEntries(path) + .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles)) + .OrderBy(i => i.FullName); return entries.Select(f => new FileSystemEntryInfo { @@ -142,6 +138,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNetworkShares() { + _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares"); return Array.Empty(); } From fd913d73e35491c64e39a76a266689c302a91b11 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 4 Jun 2020 10:10:36 -0600 Subject: [PATCH 150/724] Revert authorized endpoints to legacy api --- .../Controllers/Images/ImageByNameController.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs index fa60809773..db475d6b47 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs @@ -21,7 +21,6 @@ namespace Jellyfin.Api.Controllers.Images /// Images By Name Controller. /// [Route("Images")] - [Authorize] public class ImageByNameController : BaseJellyfinApiController { private readonly IServerApplicationPaths _applicationPaths; @@ -46,6 +45,7 @@ namespace Jellyfin.Api.Controllers.Images /// Retrieved list of images. /// An containing the list of images. [HttpGet("General")] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetGeneralImages() { @@ -61,6 +61,7 @@ namespace Jellyfin.Api.Controllers.Images /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("General/{Name}/{Type}")] + [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -70,11 +71,11 @@ namespace Jellyfin.Api.Controllers.Images ? "folder" : type; - var paths = BaseItem.SupportedImageExtensions - .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList(); + var path = BaseItem.SupportedImageExtensions + .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)) + .FirstOrDefault(System.IO.File.Exists); - var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault(); - if (path == null || !System.IO.File.Exists(path)) + if (path == null) { return NotFound(); } @@ -89,6 +90,7 @@ namespace Jellyfin.Api.Controllers.Images /// Retrieved list of images. /// An containing the list of images. [HttpGet("Ratings")] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRatingImages() { @@ -104,6 +106,7 @@ namespace Jellyfin.Api.Controllers.Images /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("Ratings/{Theme}/{Name}")] + [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -120,6 +123,7 @@ namespace Jellyfin.Api.Controllers.Images /// Image list retrieved. /// An containing the list of images. [HttpGet("MediaInfo")] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetMediaInfoImages() { @@ -135,6 +139,7 @@ namespace Jellyfin.Api.Controllers.Images /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("MediaInfo/{Theme}/{Name}")] + [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] From 6c53e36ccf1f27defae6faa5791598258bc604ab Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 4 Jun 2020 15:17:05 -0600 Subject: [PATCH 151/724] Fix Api Routing --- Jellyfin.Api/Controllers/ScheduledTasksController.cs | 8 ++++---- MediaBrowser.Common/Json/JsonDefaults.cs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 19cce974ea..e37e137d17 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -70,7 +70,7 @@ namespace Jellyfin.Api.Controllers /// Task retrieved. /// Task not found. /// An containing the task on success, or a if the task could not be found. - [HttpGet("{TaskID}")] + [HttpGet("{taskId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetTask([FromRoute] string taskId) @@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers /// Task started. /// Task not found. /// An on success, or a if the file could not be found. - [HttpPost("Running/{TaskID}")] + [HttpPost("Running/{taskId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult StartTask([FromRoute] string taskId) @@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers /// Task stopped. /// Task not found. /// An on success, or a if the file could not be found. - [HttpDelete("Running/{TaskID}")] + [HttpDelete("Running/{taskId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult StopTask([FromRoute] string taskId) @@ -142,7 +142,7 @@ namespace Jellyfin.Api.Controllers /// Task triggers updated. /// Task not found. /// An on success, or a if the file could not be found. - [HttpPost("{TaskID}/Triggers")] + [HttpPost("{taskId}/Triggers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateTask( diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index f38e2893ec..35925c3a26 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -30,6 +30,7 @@ namespace MediaBrowser.Common.Json options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); + options.Converters.Add(new JsonInt64Converter()); return options; } From 598cd94c4d5fd8911c9e30af7be47ca6eac7c975 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 6 Jun 2020 16:51:21 -0600 Subject: [PATCH 152/724] 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 153/724] 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 154/724] 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 155/724] 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 156/724] 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 157/724] 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 68e1ecaaf9626c9596c833571bb591a326449dc8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 7 Jun 2020 14:47:08 -0600 Subject: [PATCH 158/724] Remove EasyPassword from Authentication providers --- .../Users/DefaultAuthenticationProvider.cs | 24 ------------------- .../Users/InvalidAuthProvider.cs | 12 ---------- .../Authentication/IAuthenticationProvider.cs | 2 -- 3 files changed, 38 deletions(-) diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index b0c02030e3..9c5056a6b7 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -111,29 +111,5 @@ namespace Jellyfin.Server.Implementations.Users return Task.CompletedTask; } - - /// - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (newPassword != null) - { - newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - } - - /// - public string GetEasyPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); - } } } diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs index b6e65b5595..8ad2fecdb0 100644 --- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs +++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs @@ -32,17 +32,5 @@ namespace Jellyfin.Server.Implementations.Users { return Task.CompletedTask; } - - /// - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - // Nothing here - } - - /// - public string GetEasyPasswordHash(User user) - { - return string.Empty; - } } } diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index c0324a3841..da812f5512 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -11,8 +11,6 @@ namespace MediaBrowser.Controller.Authentication Task Authenticate(string username, string password); bool HasPassword(User user); Task ChangePassword(User user, string newPassword); - void ChangeEasyPassword(User user, string newPassword, string newPasswordHash); - string GetEasyPasswordHash(User user); } public interface IRequiresResolvedUser From 3d87c4c1b6bca920f444b4a16a99553d0ff6879e Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 7 Jun 2020 14:55:37 -0600 Subject: [PATCH 159/724] Fix EasyPassword setting --- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 01151e65e4..35ec78f5c5 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -262,7 +262,7 @@ namespace Jellyfin.Server.Implementations.Users /// public void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1) { - GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1); + user.EasyPassword = _cryptoProvider.CreatePasswordHash(newPassword).ToString(); UpdateUser(user); OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); From dd5579e0eb07202988a0800619e5df922c356f10 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 7 Jun 2020 22:04:59 -0600 Subject: [PATCH 160/724] Move FilterService to Jellyfin.Api --- Jellyfin.Api/Controllers/FilterController.cs | 219 +++++++++++++++++ MediaBrowser.Api/FilterService.cs | 243 ------------------- 2 files changed, 219 insertions(+), 243 deletions(-) create mode 100644 Jellyfin.Api/Controllers/FilterController.cs delete mode 100644 MediaBrowser.Api/FilterService.cs diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs new file mode 100644 index 0000000000..82fe207aef --- /dev/null +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -0,0 +1,219 @@ +#nullable enable +#pragma warning disable CA1801 + +using System; +using System.Linq; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Filters controller. + /// + [Authorize] + public class FilterController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public FilterController(ILibraryManager libraryManager, IUserManager userManager) + { + _libraryManager = libraryManager; + _userManager = userManager; + } + + /// + /// Gets legacy query filters. + /// + /// Optional. User id. + /// Optional. Parent id. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Filter by MediaType. Allows multiple, comma delimited. + /// Legacy query filters. + [HttpGet("/Items/Filters")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetQueryFiltersLegacy( + [FromQuery] Guid? userId, + [FromQuery] string? parentId, + [FromQuery] string? includeItemTypes, + [FromQuery] string? mediaTypes) + { + var parentItem = string.IsNullOrEmpty(parentId) + ? null + : _libraryManager.GetItemById(parentId); + + var user = userId == null || userId == Guid.Empty + ? null + : _userManager.GetUserById(userId.Value); + + if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) + { + parentItem = null; + } + + var item = string.IsNullOrEmpty(parentId) + ? user == null + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder() + : parentItem; + + var query = new InternalItemsQuery + { + User = user, + MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + Recursive = true, + EnableTotalRecordCount = false, + DtoOptions = new DtoOptions + { + Fields = new[] { ItemFields.Genres, ItemFields.Tags }, + EnableImages = false, + EnableUserData = false + } + }; + + var itemList = ((Folder)item!).GetItemList(query); + return new QueryFiltersLegacy + { + Years = itemList.Select(i => i.ProductionYear ?? -1) + .Where(i => i > 0) + .Distinct() + .OrderBy(i => i) + .ToArray(), + + Genres = itemList.SelectMany(i => i.Genres) + .DistinctNames() + .OrderBy(i => i) + .ToArray(), + + Tags = itemList + .SelectMany(i => i.Tags) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) + .ToArray(), + + OfficialRatings = itemList + .Select(i => i.OfficialRating) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) + .ToArray() + }; + } + + /// + /// Gets query filters. + /// + /// Optional. User id. + /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// [Unused] Optional. Filter by MediaType. Allows multiple, comma delimited. + /// Optional. Is item airing. + /// Optional. Is item movie. + /// Optional. Is item sports. + /// Optional. Is item kids. + /// Optional. Is item news. + /// Optional. Is item series. + /// Optional. Search recursive. + /// Query filters. + [HttpGet("/Items/Filters2")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetQueryFilters( + [FromQuery] Guid? userId, + [FromQuery] string? parentId, + [FromQuery] string? includeItemTypes, + [FromQuery] string? mediaTypes, + [FromQuery] bool? isAiring, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSports, + [FromQuery] bool? isKids, + [FromQuery] bool? isNews, + [FromQuery] bool? isSeries, + [FromQuery] bool? recursive) + { + var parentItem = string.IsNullOrEmpty(parentId) + ? null + : _libraryManager.GetItemById(parentId); + + var user = userId == null || userId == Guid.Empty + ? null + : _userManager.GetUserById(userId.Value); + + if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) + { + parentItem = null; + } + + var filters = new QueryFilters(); + var genreQuery = new InternalItemsQuery(user) + { + IncludeItemTypes = + (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + DtoOptions = new DtoOptions + { + Fields = Array.Empty(), + EnableImages = false, + EnableUserData = false + }, + IsAiring = isAiring, + IsMovie = isMovie, + IsSports = isSports, + IsKids = isKids, + IsNews = isNews, + IsSeries = isSeries + }; + + if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder) + { + genreQuery.AncestorIds = parentItem == null ? Array.Empty() : new[] { parentItem.Id }; + } + else + { + genreQuery.Parent = parentItem; + } + + if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) || + string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase)) + { + filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair + { + Name = i.Item1.Name, + Id = i.Item1.Id + }).ToArray(); + } + else + { + filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair + { + Name = i.Item1.Name, + Id = i.Item1.Id + }).ToArray(); + } + + return filters; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs deleted file mode 100644 index 5eb72cdb19..0000000000 --- a/MediaBrowser.Api/FilterService.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Items/Filters", "GET", Summary = "Gets branding configuration")] - public class GetQueryFiltersLegacy : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ParentId { 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 = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string MediaTypes { get; set; } - - public string[] GetMediaTypes() - { - return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public string[] GetIncludeItemTypes() - { - return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - } - - [Route("/Items/Filters2", "GET", Summary = "Gets branding configuration")] - public class GetQueryFilters : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ParentId { 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 = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string MediaTypes { get; set; } - - public string[] GetMediaTypes() - { - return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public string[] GetIncludeItemTypes() - { - return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public bool? IsAiring { get; set; } - public bool? IsMovie { get; set; } - public bool? IsSports { get; set; } - public bool? IsKids { get; set; } - public bool? IsNews { get; set; } - public bool? IsSeries { get; set; } - public bool? Recursive { get; set; } - } - - [Authenticated] - public class FilterService : BaseApiService - { - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - - public FilterService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - IUserManager userManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _libraryManager = libraryManager; - _userManager = userManager; - } - - public object Get(GetQueryFilters request) - { - var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId); - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) - { - parentItem = null; - } - - var filters = new QueryFilters(); - - var genreQuery = new InternalItemsQuery(user) - { - IncludeItemTypes = request.GetIncludeItemTypes(), - DtoOptions = new Controller.Dto.DtoOptions - { - Fields = new ItemFields[] { }, - EnableImages = false, - EnableUserData = false - }, - IsAiring = request.IsAiring, - IsMovie = request.IsMovie, - IsSports = request.IsSports, - IsKids = request.IsKids, - IsNews = request.IsNews, - IsSeries = request.IsSeries - }; - - // Non recursive not yet supported for library folders - if ((request.Recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder) - { - genreQuery.AncestorIds = parentItem == null ? Array.Empty() : new[] { parentItem.Id }; - } - else - { - genreQuery.Parent = parentItem; - } - - if (string.Equals(request.IncludeItemTypes, "MusicAlbum", StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, "MusicVideo", StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, "MusicArtist", StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, "Audio", StringComparison.OrdinalIgnoreCase)) - { - filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair - { - Name = i.Item1.Name, - Id = i.Item1.Id - - }).ToArray(); - } - else - { - filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair - { - Name = i.Item1.Name, - Id = i.Item1.Id - - }).ToArray(); - } - - return ToOptimizedResult(filters); - } - - public object Get(GetQueryFiltersLegacy request) - { - var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId); - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) || - string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) - { - parentItem = null; - } - - var item = string.IsNullOrEmpty(request.ParentId) ? - user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() : - parentItem; - - var result = ((Folder)item).GetItemList(GetItemsQuery(request, user)); - - var filters = GetFilters(result); - - return ToOptimizedResult(filters); - } - - private QueryFiltersLegacy GetFilters(IReadOnlyCollection items) - { - var result = new QueryFiltersLegacy(); - - result.Years = items.Select(i => i.ProductionYear ?? -1) - .Where(i => i > 0) - .Distinct() - .OrderBy(i => i) - .ToArray(); - - result.Genres = items.SelectMany(i => i.Genres) - .DistinctNames() - .OrderBy(i => i) - .ToArray(); - - result.Tags = items - .SelectMany(i => i.Tags) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) - .ToArray(); - - result.OfficialRatings = items - .Select(i => i.OfficialRating) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) - .ToArray(); - - return result; - } - - private InternalItemsQuery GetItemsQuery(GetQueryFiltersLegacy request, User user) - { - var query = new InternalItemsQuery - { - User = user, - MediaTypes = request.GetMediaTypes(), - IncludeItemTypes = request.GetIncludeItemTypes(), - Recursive = true, - EnableTotalRecordCount = false, - DtoOptions = new Controller.Dto.DtoOptions - { - Fields = new[] { ItemFields.Genres, ItemFields.Tags }, - EnableImages = false, - EnableUserData = false - } - }; - - return query; - } - } -} From 16e26be87f3c17422b7467882c5170fe11031825 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 8 Jun 2020 07:22:40 -0600 Subject: [PATCH 161/724] Move ItemLookupService to Jellyfin.Api --- .../Controllers/ItemLookupController.cs | 364 ++++++++++++++++++ MediaBrowser.Api/ItemLookupService.cs | 332 ---------------- 2 files changed, 364 insertions(+), 332 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ItemLookupController.cs delete mode 100644 MediaBrowser.Api/ItemLookupService.cs diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs new file mode 100644 index 0000000000..e474f2b23d --- /dev/null +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -0,0 +1,364 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Net.Mime; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Providers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Item lookup controller. + /// + [Authorize] + public class ItemLookupController : BaseJellyfinApiController + { + private readonly IProviderManager _providerManager; + private readonly IServerApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ItemLookupController( + IProviderManager providerManager, + IServerConfigurationManager serverConfigurationManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + ILogger logger) + { + _providerManager = providerManager; + _appPaths = serverConfigurationManager.ApplicationPaths; + _fileSystem = fileSystem; + _libraryManager = libraryManager; + _logger = logger; + } + + /// + /// Get the item's external id info. + /// + /// Item id. + /// External id info retrieved. + /// Item not found. + /// List of external id info. + [HttpGet("/Items/{itemId}/ExternalIdInfos")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetExternalIdInfos([FromRoute] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + return Ok(_providerManager.GetExternalIdInfos(item)); + } + + /// + /// Get movie remote search. + /// + /// Remote search query. + /// Movie remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/Movie")] + public async Task>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get trailer remote search. + /// + /// Remote search query. + /// Trailer remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/Trailer")] + public async Task>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get music video remote search. + /// + /// Remote search query. + /// Music video remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/MusicVideo")] + public async Task>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get series remote search. + /// + /// Remote search query. + /// Series remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/Series")] + public async Task>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get box set remote search. + /// + /// Remote search query. + /// Box set remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/BoxSet")] + public async Task>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get music artist remote search. + /// + /// Remote search query. + /// Music artist remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/MusicArtist")] + public async Task>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get music album remote search. + /// + /// Remote search query. + /// Music album remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/MusicAlbum")] + public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get person remote search. + /// + /// Remote search query. + /// Person remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/Person")] + [Authorize(Policy = Policies.RequiresElevation)] + public async Task>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get book remote search. + /// + /// Remote search query. + /// Book remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("/Items/RemoteSearch/Book")] + public async Task>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Gets a remote image. + /// + /// The image url. + /// The provider name. + /// Remote image retrieved. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the images file stream. + /// + [HttpGet("/Items/RemoteSearch/Image")] + public async Task GetRemoteSearchImage( + [FromQuery, Required] string imageUrl, + [FromQuery, Required] string providerName) + { + var urlHash = imageUrl.GetMD5(); + var pointerCachePath = GetFullCachePath(urlHash.ToString()); + + try + { + var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); + if (System.IO.File.Exists(contentPath)) + { + await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath); + return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet); + } + } + catch (FileNotFoundException) + { + // Means the file isn't cached yet + } + catch (IOException) + { + // Means the file isn't cached yet + } + + await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); + + // Read the pointer file again + await using var fileStream = System.IO.File.OpenRead(pointerCachePath); + return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet); + } + + /// + /// Applies search criteria to an item and refreshes metadata. + /// + /// Item id. + /// The remote search result. + /// Optional. Whether or not to replace all images. Default: True. + /// Item metadata refreshed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an . + /// + [HttpPost("/Items/RemoteSearch/Apply/{id}")] + [Authorize(Policy = Policies.RequiresElevation)] + public async Task ApplySearchCriteria( + [FromRoute] Guid itemId, + [FromBody, BindRequired] RemoteSearchResult searchResult, + [FromQuery] bool replaceAllImages = true) + { + var item = _libraryManager.GetItemById(itemId); + _logger.LogInformation( + "Setting provider id's to item {0}-{1}: {2}", + item.Id, + item.Name, + JsonSerializer.Serialize(searchResult.ProviderIds)); + + // Since the refresh process won't erase provider Ids, we need to set this explicitly now. + item.ProviderIds = searchResult.ProviderIds; + await _providerManager.RefreshFullItem( + item, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ReplaceAllMetadata = true, + ReplaceAllImages = replaceAllImages, + SearchResult = searchResult + }, CancellationToken.None).ConfigureAwait(false); + + return Ok(); + } + + /// + /// Downloads the image. + /// + /// Name of the provider. + /// The URL. + /// The URL hash. + /// The pointer cache path. + /// Task. + private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) + { + var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); + var ext = result.ContentType.Split('/').Last(); + var fullCachePath = GetFullCachePath(urlHash + "." + ext); + + Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); + await using (var stream = result.Content) + { + await using var fileStream = new FileStream( + fullCachePath, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + IODefaults.FileStreamBufferSize, + true); + + await stream.CopyToAsync(fileStream).ConfigureAwait(false); + } + + Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); + await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false); + } + + /// + /// Gets the full cache path. + /// + /// The filename. + /// System.String. + private string GetFullCachePath(string filename) + => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs deleted file mode 100644 index 0bbe7e1cfa..0000000000 --- a/MediaBrowser.Api/ItemLookupService.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Items/{Id}/ExternalIdInfos", "GET", Summary = "Gets external id infos for an item")] - [Authenticated(Roles = "Admin")] - public class GetExternalIdInfos : IReturn> - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - } - - [Route("/Items/RemoteSearch/Movie", "POST")] - [Authenticated] - public class GetMovieRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Trailer", "POST")] - [Authenticated] - public class GetTrailerRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/MusicVideo", "POST")] - [Authenticated] - public class GetMusicVideoRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Series", "POST")] - [Authenticated] - public class GetSeriesRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/BoxSet", "POST")] - [Authenticated] - public class GetBoxSetRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/MusicArtist", "POST")] - [Authenticated] - public class GetMusicArtistRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/MusicAlbum", "POST")] - [Authenticated] - public class GetMusicAlbumRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Person", "POST")] - [Authenticated(Roles = "Admin")] - public class GetPersonRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Book", "POST")] - [Authenticated] - public class GetBookRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Image", "GET", Summary = "Gets a remote image")] - public class GetRemoteSearchImage - { - [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ImageUrl { get; set; } - - [ApiMember(Name = "ProviderName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ProviderName { get; set; } - } - - [Route("/Items/RemoteSearch/Apply/{Id}", "POST", Summary = "Applies search criteria to an item and refreshes metadata")] - [Authenticated(Roles = "Admin")] - public class ApplySearchCriteria : RemoteSearchResult, IReturnVoid - { - [ApiMember(Name = "Id", Description = "The item id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "ReplaceAllImages", Description = "Whether or not to replace all images", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] - public bool ReplaceAllImages { get; set; } - - public ApplySearchCriteria() - { - ReplaceAllImages = true; - } - } - - public class ItemLookupService : BaseApiService - { - private readonly IProviderManager _providerManager; - private readonly IServerApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; - private readonly IJsonSerializer _json; - - public ItemLookupService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager, - IJsonSerializer json) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _providerManager = providerManager; - _appPaths = serverConfigurationManager.ApplicationPaths; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - _json = json; - } - - public object Get(GetExternalIdInfos request) - { - var item = _libraryManager.GetItemById(request.Id); - - var infos = _providerManager.GetExternalIdInfos(item).ToList(); - - return ToOptimizedResult(infos); - } - - public async Task Post(GetTrailerRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetBookRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMovieRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetSeriesRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetBoxSetRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMusicVideoRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetPersonRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMusicAlbumRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMusicArtistRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public Task Get(GetRemoteSearchImage request) - { - return GetRemoteImage(request); - } - - public Task Post(ApplySearchCriteria request) - { - var item = _libraryManager.GetItemById(new Guid(request.Id)); - - //foreach (var key in request.ProviderIds) - //{ - // var value = key.Value; - - // if (!string.IsNullOrWhiteSpace(value)) - // { - // item.SetProviderId(key.Key, value); - // } - //} - Logger.LogInformation("Setting provider id's to item {0}-{1}: {2}", item.Id, item.Name, _json.SerializeToString(request.ProviderIds)); - - // Since the refresh process won't erase provider Ids, we need to set this explicitly now. - item.ProviderIds = request.ProviderIds; - //item.ProductionYear = request.ProductionYear; - //item.Name = request.Name; - - return _providerManager.RefreshFullItem( - item, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true, - ReplaceAllImages = request.ReplaceAllImages, - SearchResult = request - }, - CancellationToken.None); - } - - /// - /// Gets the remote image. - /// - /// The request. - /// Task{System.Object}. - private async Task GetRemoteImage(GetRemoteSearchImage request) - { - var urlHash = request.ImageUrl.GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string contentPath; - - try - { - contentPath = File.ReadAllText(pointerCachePath); - - if (File.Exists(contentPath)) - { - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - } - catch (FileNotFoundException) - { - // Means the file isn't cached yet - } - catch (IOException) - { - // Means the file isn't cached yet - } - - await DownloadImage(request.ProviderName, request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - - // Read the pointer file again - contentPath = File.ReadAllText(pointerCachePath); - - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - - /// - /// Downloads the image. - /// - /// Name of the provider. - /// The URL. - /// The URL hash. - /// The pointer cache path. - /// Task. - private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) - { - var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); - - var ext = result.ContentType.Split('/').Last(); - - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) - { - using var fileStream = new FileStream( - fullCachePath, - FileMode.Create, - FileAccess.Write, - FileShare.Read, - IODefaults.FileStreamBufferSize, - true); - - await stream.CopyToAsync(fileStream).ConfigureAwait(false); - } - - Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); - File.WriteAllText(pointerCachePath, fullCachePath); - } - - /// - /// Gets the full cache path. - /// - /// The filename. - /// System.String. - private string GetFullCachePath(string filename) - => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); - } -} From 6f64e48c540db93053961a028b95e8f0bbb64076 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 8 Jun 2020 07:37:16 -0600 Subject: [PATCH 162/724] Add response codes --- Jellyfin.Api/Controllers/FilterController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 82fe207aef..d06c5e96c9 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -44,6 +44,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Parent id. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. /// Optional. Filter by MediaType. Allows multiple, comma delimited. + /// Legacy filters retrieved. /// Legacy query filters. [HttpGet("/Items/Filters")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Is item news. /// Optional. Is item series. /// Optional. Search recursive. + /// Filters retrieved. /// Query filters. [HttpGet("/Items/Filters2")] [ProducesResponseType(StatusCodes.Status200OK)] From 81d3ec7205dd559e6444c17f9ecefc37977a5911 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 8 Jun 2020 12:20:33 -0600 Subject: [PATCH 163/724] Move ItemUpdateService to Jellyfin.Api --- .../Controllers/ItemUpdateController.cs | 423 ++++++++++-------- 1 file changed, 237 insertions(+), 186 deletions(-) rename MediaBrowser.Api/ItemUpdateService.cs => Jellyfin.Api/Controllers/ItemUpdateController.cs (63%) diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs similarity index 63% rename from MediaBrowser.Api/ItemUpdateService.cs rename to Jellyfin.Api/Controllers/ItemUpdateController.cs index 2db6d717aa..0c5fece832 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -1,215 +1,101 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using Jellyfin.Api.Constants; using MediaBrowser.Controller.Configuration; 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.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace MediaBrowser.Api +namespace Jellyfin.Api.Controllers { - [Route("/Items/{ItemId}", "POST", Summary = "Updates an item")] - public class UpdateItem : BaseItemDto, IReturnVoid - { - [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string ItemId { get; set; } - } - - [Route("/Items/{ItemId}/MetadataEditor", "GET", Summary = "Gets metadata editor info for an item")] - public class GetMetadataEditorInfo : IReturn - { - [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string ItemId { get; set; } - } - - [Route("/Items/{ItemId}/ContentType", "POST", Summary = "Updates an item's content type")] - public class UpdateItemContentType : IReturnVoid - { - [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid ItemId { get; set; } - - [ApiMember(Name = "ContentType", Description = "The content type of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ContentType { get; set; } - } - - [Authenticated(Roles = "admin")] - public class ItemUpdateService : BaseApiService + /// + /// Item update controller. + /// + [Authorize(Policy = Policies.RequiresElevation)] + public class ItemUpdateController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly ILocalizationManager _localizationManager; private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _serverConfigurationManager; - public ItemUpdateService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ItemUpdateController( IFileSystem fileSystem, ILibraryManager libraryManager, IProviderManager providerManager, - ILocalizationManager localizationManager) - : base(logger, serverConfigurationManager, httpResultFactory) + ILocalizationManager localizationManager, + IServerConfigurationManager serverConfigurationManager) { _libraryManager = libraryManager; _providerManager = providerManager; _localizationManager = localizationManager; _fileSystem = fileSystem; + _serverConfigurationManager = serverConfigurationManager; } - public object Get(GetMetadataEditorInfo request) + /// + /// Updates an item. + /// + /// The item id. + /// The new item properties. + /// Item updated. + /// Item not found. + /// An on success, or a if the item could not be found. + [HttpPost("/Items/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request) { - var item = _libraryManager.GetItemById(request.ItemId); - - var info = new MetadataEditorInfo + var item = _libraryManager.GetItemById(itemId); + if (item == null) { - ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), - ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), - Countries = _localizationManager.GetCountries().ToArray(), - Cultures = _localizationManager.GetCultures().ToArray() - }; - - if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName) && - item.SourceType == SourceType.Library) - { - var inheritedContentType = _libraryManager.GetInheritedContentType(item); - var configuredContentType = _libraryManager.GetConfiguredContentType(item); - - if (string.IsNullOrWhiteSpace(inheritedContentType) || !string.IsNullOrWhiteSpace(configuredContentType)) - { - info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); - info.ContentType = configuredContentType; - - if (string.IsNullOrWhiteSpace(inheritedContentType) || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) - { - info.ContentTypeOptions = info.ContentTypeOptions - .Where(i => string.IsNullOrWhiteSpace(i.Value) || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - } - } + return NotFound(); } - return ToOptimizedResult(info); - } - - public void Post(UpdateItemContentType request) - { - var item = _libraryManager.GetItemById(request.ItemId); - var path = item.ContainingFolderPath; - - var types = ServerConfigurationManager.Configuration.ContentTypes - .Where(i => !string.IsNullOrWhiteSpace(i.Name)) - .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (!string.IsNullOrWhiteSpace(request.ContentType)) - { - types.Add(new NameValuePair - { - Name = path, - Value = request.ContentType - }); - } - - ServerConfigurationManager.Configuration.ContentTypes = types.ToArray(); - ServerConfigurationManager.SaveConfiguration(); - } - - private List GetContentTypeOptions(bool isForItem) - { - var list = new List(); - - if (isForItem) - { - list.Add(new NameValuePair - { - Name = "Inherit", - Value = "" - }); - } - - list.Add(new NameValuePair - { - Name = "Movies", - Value = "movies" - }); - list.Add(new NameValuePair - { - Name = "Music", - Value = "music" - }); - list.Add(new NameValuePair - { - Name = "Shows", - Value = "tvshows" - }); - - if (!isForItem) - { - list.Add(new NameValuePair - { - Name = "Books", - Value = "books" - }); - } - - list.Add(new NameValuePair - { - Name = "HomeVideos", - Value = "homevideos" - }); - list.Add(new NameValuePair - { - Name = "MusicVideos", - Value = "musicvideos" - }); - list.Add(new NameValuePair - { - Name = "Photos", - Value = "photos" - }); - - if (!isForItem) - { - list.Add(new NameValuePair - { - Name = "MixedContent", - Value = "" - }); - } - - foreach (var val in list) - { - val.Name = _localizationManager.GetLocalizedString(val.Name); - } - - return list; - } - - public void Post(UpdateItem request) - { - var item = _libraryManager.GetItemById(request.ItemId); - var newLockData = request.LockData ?? false; var isLockedChanged = item.IsLocked != newLockData; var series = item as Series; - var displayOrderChanged = series != null && !string.Equals(series.DisplayOrder ?? string.Empty, request.DisplayOrder ?? string.Empty, StringComparison.OrdinalIgnoreCase); + var displayOrderChanged = series != null && !string.Equals( + series.DisplayOrder ?? string.Empty, + request.DisplayOrder ?? string.Empty, + StringComparison.OrdinalIgnoreCase); // Do this first so that metadata savers can pull the updates from the database. if (request.People != null) { - _libraryManager.UpdatePeople(item, request.People.Select(x => new PersonInfo { Name = x.Name, Role = x.Role, Type = x.Type }).ToList()); + _libraryManager.UpdatePeople( + item, + request.People.Select(x => new PersonInfo + { + Name = x.Name, + Role = x.Role, + Type = x.Type + }).ToList()); } UpdateItem(request, item); @@ -232,7 +118,7 @@ namespace MediaBrowser.Api if (displayOrderChanged) { _providerManager.QueueRefresh( - series.Id, + series!.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = MetadataRefreshMode.FullRefresh, @@ -241,11 +127,99 @@ namespace MediaBrowser.Api }, RefreshPriority.High); } + + return Ok(); } - private DateTime NormalizeDateTime(DateTime val) + /// + /// Gets metadata editor info for an item. + /// + /// The item id. + /// Item metadata editor returned. + /// Item not found. + /// An on success containing the metadata editor, or a if the item could not be found. + [HttpGet("/Items/{itemId}/MetadataEditor")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetMetadataEditorInfo([FromRoute] Guid itemId) { - return DateTime.SpecifyKind(val, DateTimeKind.Utc); + var item = _libraryManager.GetItemById(itemId); + + var info = new MetadataEditorInfo + { + ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), + ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), + Countries = _localizationManager.GetCountries().ToArray(), + Cultures = _localizationManager.GetCultures().ToArray() + }; + + if (!item.IsVirtualItem + && !(item is ICollectionFolder) + && !(item is UserView) + && !(item is AggregateFolder) + && !(item is LiveTvChannel) + && !(item is IItemByName) + && item.SourceType == SourceType.Library) + { + var inheritedContentType = _libraryManager.GetInheritedContentType(item); + var configuredContentType = _libraryManager.GetConfiguredContentType(item); + + if (string.IsNullOrWhiteSpace(inheritedContentType) || + !string.IsNullOrWhiteSpace(configuredContentType)) + { + info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); + info.ContentType = configuredContentType; + + if (string.IsNullOrWhiteSpace(inheritedContentType) + || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + { + info.ContentTypeOptions = info.ContentTypeOptions + .Where(i => string.IsNullOrWhiteSpace(i.Value) + || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + } + } + + return info; + } + + /// + /// Updates an item's content type. + /// + /// The item id. + /// The content type of the item. + /// Item content type updated. + /// Item not found. + /// An on success, or a if the item could not be found. + [HttpPost("/Items/{itemId}/ContentType")] + public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + var path = item.ContainingFolderPath; + + var types = _serverConfigurationManager.Configuration.ContentTypes + .Where(i => !string.IsNullOrWhiteSpace(i.Name)) + .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (!string.IsNullOrWhiteSpace(contentType)) + { + types.Add(new NameValuePair + { + Name = path, + Value = contentType + }); + } + + _serverConfigurationManager.Configuration.ContentTypes = types.ToArray(); + _serverConfigurationManager.SaveConfiguration(); + return Ok(); } private void UpdateItem(BaseItemDto request, BaseItem item) @@ -361,24 +335,25 @@ namespace MediaBrowser.Api } } - if (item is Audio song) + switch (item) { - song.Album = request.Album; - } - - if (item is MusicVideo musicVideo) - { - musicVideo.Album = request.Album; - } - - if (item is Series series) - { - series.Status = GetSeriesStatus(request); - - if (request.AirDays != null) + case Audio song: + song.Album = request.Album; + break; + case MusicVideo musicVideo: + musicVideo.Album = request.Album; + break; + case Series series: { - series.AirDays = request.AirDays; - series.AirTime = request.AirTime; + series.Status = GetSeriesStatus(request); + + if (request.AirDays != null) + { + series.AirDays = request.AirDays; + series.AirTime = request.AirTime; + } + + break; } } } @@ -392,5 +367,81 @@ namespace MediaBrowser.Api return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true); } + + private DateTime NormalizeDateTime(DateTime val) + { + return DateTime.SpecifyKind(val, DateTimeKind.Utc); + } + + private List GetContentTypeOptions(bool isForItem) + { + var list = new List(); + + if (isForItem) + { + list.Add(new NameValuePair + { + Name = "Inherit", + Value = string.Empty + }); + } + + list.Add(new NameValuePair + { + Name = "Movies", + Value = "movies" + }); + list.Add(new NameValuePair + { + Name = "Music", + Value = "music" + }); + list.Add(new NameValuePair + { + Name = "Shows", + Value = "tvshows" + }); + + if (!isForItem) + { + list.Add(new NameValuePair + { + Name = "Books", + Value = "books" + }); + } + + list.Add(new NameValuePair + { + Name = "HomeVideos", + Value = "homevideos" + }); + list.Add(new NameValuePair + { + Name = "MusicVideos", + Value = "musicvideos" + }); + list.Add(new NameValuePair + { + Name = "Photos", + Value = "photos" + }); + + if (!isForItem) + { + list.Add(new NameValuePair + { + Name = "MixedContent", + Value = string.Empty + }); + } + + foreach (var val in list) + { + val.Name = _localizationManager.GetLocalizedString(val.Name); + } + + return list; + } } } From a4455af3e90b440defb4da6de980f287b8a348c6 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 8 Jun 2020 12:34:17 -0600 Subject: [PATCH 164/724] Move LocalizationService to Jellyfin.Api --- .../Controllers/LocalizationController.cs | 76 ++++++++++++ MediaBrowser.Api/LocalizationService.cs | 111 ------------------ 2 files changed, 76 insertions(+), 111 deletions(-) create mode 100644 Jellyfin.Api/Controllers/LocalizationController.cs delete mode 100644 MediaBrowser.Api/LocalizationService.cs diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs new file mode 100644 index 0000000000..1466dd3ec0 --- /dev/null +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using Jellyfin.Api.Constants; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Localization controller. + /// + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + public class LocalizationController : BaseJellyfinApiController + { + private readonly ILocalizationManager _localization; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public LocalizationController(ILocalizationManager localization) + { + _localization = localization; + } + + /// + /// Gets known cultures. + /// + /// Known cultures returned. + /// An containing the list of cultures. + [HttpGet("Cultures")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetCultures() + { + return Ok(_localization.GetCultures()); + } + + /// + /// Gets known countries. + /// + /// Known countries returned. + /// An containing the list of countries. + [HttpGet("Countries")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetCountries() + { + return Ok(_localization.GetCountries()); + } + + /// + /// Gets known parental ratings. + /// + /// Known parental ratings returned. + /// An containing the list of parental ratings. + [HttpGet("ParentalRatings")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetParentalRatings() + { + return Ok(_localization.GetParentalRatings()); + } + + /// + /// Gets localization options. + /// + /// Localization options returned. + /// An containing the list of localization options. + [HttpGet("Options")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLocalizationOptions() + { + return Ok(_localization.GetLocalizationOptions()); + } + } +} diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs deleted file mode 100644 index 6a69d26568..0000000000 --- a/MediaBrowser.Api/LocalizationService.cs +++ /dev/null @@ -1,111 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetCultures - /// - [Route("/Localization/Cultures", "GET", Summary = "Gets known cultures")] - public class GetCultures : IReturn - { - } - - /// - /// Class GetCountries - /// - [Route("/Localization/Countries", "GET", Summary = "Gets known countries")] - public class GetCountries : IReturn - { - } - - /// - /// Class ParentalRatings - /// - [Route("/Localization/ParentalRatings", "GET", Summary = "Gets known parental ratings")] - public class GetParentalRatings : IReturn - { - } - - /// - /// Class ParentalRatings - /// - [Route("/Localization/Options", "GET", Summary = "Gets localization options")] - public class GetLocalizationOptions : IReturn - { - } - - /// - /// Class CulturesService - /// - [Authenticated(AllowBeforeStartupWizard = true)] - public class LocalizationService : BaseApiService - { - /// - /// The _localization - /// - private readonly ILocalizationManager _localization; - - /// - /// Initializes a new instance of the class. - /// - /// The localization. - public LocalizationService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILocalizationManager localization) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _localization = localization; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetParentalRatings request) - { - var result = _localization.GetParentalRatings(); - - return ToOptimizedResult(result); - } - - public object Get(GetLocalizationOptions request) - { - var result = _localization.GetLocalizationOptions(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetCountries request) - { - var result = _localization.GetCountries(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetCultures request) - { - var result = _localization.GetCultures(); - - return ToOptimizedResult(result); - } - } - -} From d97e306cdae11eb2675161aa2a6d828c739b2b01 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 8 Jun 2020 13:14:41 -0600 Subject: [PATCH 165/724] Move PlaylistService to Jellyfin.Api --- .../Controllers/PlaylistsController.cs | 198 ++++++++++++++++++ Jellyfin.Api/Helpers/RequestHelpers.cs | 30 +++ .../Models/PlaylistDtos/CreatePlaylistDto.cs | 31 +++ MediaBrowser.Api/PlaylistService.cs | 74 ------- 4 files changed, 259 insertions(+), 74 deletions(-) create mode 100644 Jellyfin.Api/Controllers/PlaylistsController.cs create mode 100644 Jellyfin.Api/Helpers/RequestHelpers.cs create mode 100644 Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs new file mode 100644 index 0000000000..0d73962de4 --- /dev/null +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -0,0 +1,198 @@ +#nullable enable +#pragma warning disable CA1801 + +using System; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.PlaylistDtos; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Playlists; +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 +{ + /// + /// Playlists controller. + /// + [Authorize] + public class PlaylistsController : BaseJellyfinApiController + { + private readonly IPlaylistManager _playlistManager; + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public PlaylistsController( + IDtoService dtoService, + IPlaylistManager playlistManager, + IUserManager userManager, + ILibraryManager libraryManager) + { + _dtoService = dtoService; + _playlistManager = playlistManager; + _userManager = userManager; + _libraryManager = libraryManager; + } + + /// + /// Creates a new playlist. + /// + /// The create playlist payload. + /// + /// A that represents the asynchronous operation to create a playlist. + /// The task result contains an indicating success. + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreatePlaylist( + [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest) + { + Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids); + var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest + { + Name = createPlaylistRequest.Name, + ItemIdList = idGuidArray, + UserId = createPlaylistRequest.UserId, + MediaType = createPlaylistRequest.MediaType + }).ConfigureAwait(false); + + return result; + } + + /// + /// Adds items to a playlist. + /// + /// The playlist id. + /// Item id, comma delimited. + /// The userId. + /// Items added to playlist. + /// An on success. + [HttpPost("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult AddToPlaylist( + [FromRoute] string playlistId, + [FromQuery] string ids, + [FromQuery] Guid userId) + { + _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId); + return Ok(); + } + + /// + /// Moves a playlist item. + /// + /// The playlist id. + /// The item id. + /// The new index. + /// Item moved to new index. + /// An on success. + [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult MoveItem( + [FromRoute] string playlistId, + [FromRoute] string itemId, + [FromRoute] int newIndex) + { + _playlistManager.MoveItem(playlistId, itemId, newIndex); + return Ok(); + } + + /// + /// Removes items from a playlist. + /// + /// The playlist id. + /// The item ids, comma delimited. + /// Items removed. + /// An on success. + [HttpDelete("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds) + { + _playlistManager.RemoveFromPlaylist(playlistId, entryIds.Split(',')); + return Ok(); + } + + /// + /// Gets the original items of a playlist. + /// + /// The playlist id. + /// User id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Original playlist returned. + /// Playlist not found. + /// The original playlist items. + [HttpGet("{playlistId}/Items")] + public ActionResult> GetPlaylistItems( + [FromRoute] Guid playlistId, + [FromRoute] Guid userId, + [FromRoute] int? startIndex, + [FromRoute] int? limit, + [FromRoute] string fields, + [FromRoute] bool? enableImages, + [FromRoute] bool? enableUserData, + [FromRoute] bool? imageTypeLimit, + [FromRoute] string enableImageTypes) + { + var playlist = (Playlist)_libraryManager.GetItemById(playlistId); + if (playlist == null) + { + return NotFound(); + } + + var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + + var items = playlist.GetManageableItems().ToArray(); + + var count = items.Length; + + if (startIndex.HasValue) + { + items = items.Skip(startIndex.Value).ToArray(); + } + + if (limit.HasValue) + { + items = items.Take(limit.Value).ToArray(); + } + + // TODO var dtoOptions = GetDtoOptions(_authContext, request); + var dtoOptions = new DtoOptions(); + + var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); + + for (int index = 0; index < dtos.Count; index++) + { + dtos[index].PlaylistItemId = items[index].Item1.Id; + } + + var result = new QueryResult + { + Items = dtos, + TotalRecordCount = count + }; + + return result; + } + } +} diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs new file mode 100644 index 0000000000..b1c6a24d0e --- /dev/null +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -0,0 +1,30 @@ +#nullable enable + +using System; +using System.Linq; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Request Helpers. + /// + public static class RequestHelpers + { + /// + /// Get Guid array from string. + /// + /// String value. + /// Guid array. + public static Guid[] GetGuids(string? value) + { + if (value == null) + { + return Array.Empty(); + } + + return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(i => new Guid(i)) + .ToArray(); + } + } +} diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs new file mode 100644 index 0000000000..20835eecbd --- /dev/null +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -0,0 +1,31 @@ +#nullable enable +using System; + +namespace Jellyfin.Api.Models.PlaylistDtos +{ + /// + /// Create new playlist dto. + /// + public class CreatePlaylistDto + { + /// + /// Gets or sets the name of the new playlist. + /// + public string? Name { get; set; } + + /// + /// Gets or sets item ids to add to the playlist. + /// + public string? Ids { get; set; } + + /// + /// Gets or sets the user id. + /// + public Guid UserId { get; set; } + + /// + /// Gets or sets the media type. + /// + public string? MediaType { get; set; } + } +} diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index 953b00e35a..f4fa8955b7 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -14,66 +14,6 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { - [Route("/Playlists", "POST", Summary = "Creates a new playlist")] - public class CreatePlaylist : IReturn - { - [ApiMember(Name = "Name", Description = "The name of the new playlist.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Name { get; set; } - - [ApiMember(Name = "Ids", Description = "Item Ids to add to the playlist", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } - - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid UserId { get; set; } - - [ApiMember(Name = "MediaType", Description = "The playlist media type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string MediaType { get; set; } - } - - [Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")] - public class AddToPlaylist : IReturnVoid - { - [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Ids { get; set; } - - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public Guid UserId { get; set; } - } - - [Route("/Playlists/{Id}/Items/{ItemId}/Move/{NewIndex}", "POST", Summary = "Moves a playlist item")] - public class MoveItem : IReturnVoid - { - [ApiMember(Name = "ItemId", Description = "ItemId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemId { get; set; } - - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "NewIndex", Description = "NewIndex", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public int NewIndex { get; set; } - } - - [Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")] - public class RemoveFromPlaylist : IReturnVoid - { - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - - [ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string EntryIds { get; set; } - } - [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")] public class GetPlaylistItems : IReturn>, IHasDtoOptions { @@ -153,20 +93,6 @@ namespace MediaBrowser.Api _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex); } - public async Task Post(CreatePlaylist request) - { - var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest - { - Name = request.Name, - ItemIdList = GetGuids(request.Ids), - UserId = request.UserId, - MediaType = request.MediaType - - }).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - public void Post(AddToPlaylist request) { _playlistManager.AddToPlaylist(request.Id, GetGuids(request.Ids), request.UserId); From 7a77b9928f2c8326e85629d3c900e86c3b26342a Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 8 Jun 2020 13:14:55 -0600 Subject: [PATCH 166/724] Move PlaylistService to Jellyfin.Api --- MediaBrowser.Api/PlaylistService.cs | 143 ---------------------------- 1 file changed, 143 deletions(-) delete mode 100644 MediaBrowser.Api/PlaylistService.cs diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs deleted file mode 100644 index f4fa8955b7..0000000000 --- a/MediaBrowser.Api/PlaylistService.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Playlists; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Playlists; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")] - public class GetPlaylistItems : IReturn>, IHasDtoOptions - { - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// 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; } - - /// - /// Fields to return within the items, in addition to basic information - /// - /// The fields. - [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string Fields { get; set; } - - [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? EnableImages { get; set; } - - [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? EnableUserData { get; set; } - - [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? ImageTypeLimit { get; set; } - - [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string EnableImageTypes { get; set; } - } - - [Authenticated] - public class PlaylistService : BaseApiService - { - private readonly IPlaylistManager _playlistManager; - private readonly IDtoService _dtoService; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IAuthorizationContext _authContext; - - public PlaylistService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IDtoService dtoService, - IPlaylistManager playlistManager, - IUserManager userManager, - ILibraryManager libraryManager, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _dtoService = dtoService; - _playlistManager = playlistManager; - _userManager = userManager; - _libraryManager = libraryManager; - _authContext = authContext; - } - - public void Post(MoveItem request) - { - _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex); - } - - public void Post(AddToPlaylist request) - { - _playlistManager.AddToPlaylist(request.Id, GetGuids(request.Ids), request.UserId); - } - - public void Delete(RemoveFromPlaylist request) - { - _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(',')); - } - - public object Get(GetPlaylistItems request) - { - var playlist = (Playlist)_libraryManager.GetItemById(request.Id); - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var items = playlist.GetManageableItems().ToArray(); - - var count = items.Length; - - if (request.StartIndex.HasValue) - { - items = items.Skip(request.StartIndex.Value).ToArray(); - } - - if (request.Limit.HasValue) - { - items = items.Take(request.Limit.Value).ToArray(); - } - - var dtoOptions = GetDtoOptions(_authContext, request); - - var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); - - for (int index = 0; index < dtos.Count; index++) - { - dtos[index].PlaylistItemId = items[index].Item1.Id; - } - - var result = new QueryResult - { - Items = dtos, - TotalRecordCount = count - }; - - return ToOptimizedResult(result); - } - } -} From 1491e93d841a8c40ea1a9fed9d99427a64ccd66c Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 9 Jun 2020 11:00:37 +0200 Subject: [PATCH 167/724] 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 0b778b88516e59aafeaf5e6b8b0a117e0dd68607 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 9 Jun 2020 09:57:01 -0600 Subject: [PATCH 168/724] Move PluginService to Jellyfin.Api --- Jellyfin.Api/Controllers/PluginsController.cs | 189 ++++++++++++ .../Models/PluginDtos/MBRegistrationRecord.cs | 42 +++ .../Models/PluginDtos/PluginSecurityInfo.cs | 20 ++ MediaBrowser.Api/PluginService.cs | 268 ------------------ 4 files changed, 251 insertions(+), 268 deletions(-) create mode 100644 Jellyfin.Api/Controllers/PluginsController.cs create mode 100644 Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs create mode 100644 Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs delete mode 100644 MediaBrowser.Api/PluginService.cs diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs new file mode 100644 index 0000000000..59196a41aa --- /dev/null +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -0,0 +1,189 @@ +#nullable enable +#pragma warning disable CA1801 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.PluginDtos; +using MediaBrowser.Common; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Updates; +using MediaBrowser.Model.Plugins; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Plugins controller. + /// + [Authorize] + public class PluginsController : BaseJellyfinApiController + { + private readonly IApplicationHost _appHost; + private readonly IInstallationManager _installationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public PluginsController( + IApplicationHost appHost, + IInstallationManager installationManager) + { + _appHost = appHost; + _installationManager = installationManager; + } + + /// + /// Gets a list of currently installed plugins. + /// + /// Optional. Unused. + /// Installed plugins returned. + /// List of currently installed plugins. + [HttpGet] + public ActionResult> GetPlugins([FromRoute] bool? isAppStoreEnabled) + { + return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); + } + + /// + /// Uninstalls a plugin. + /// + /// Plugin id. + /// Plugin uninstalled. + /// Plugin not found. + /// An on success, or a if the file could not be found. + [HttpDelete("{pluginId}")] + [Authorize(Policy = Policies.RequiresElevation)] + public ActionResult UninstallPlugin([FromRoute] Guid pluginId) + { + var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); + if (plugin == null) + { + return NotFound(); + } + + _installationManager.UninstallPlugin(plugin); + return Ok(); + } + + /// + /// Gets plugin configuration. + /// + /// Plugin id. + /// Plugin configuration returned. + /// Plugin not found or plugin configuration not found. + /// Plugin configuration. + [HttpGet("{pluginId}/Configuration")] + public ActionResult GetPluginConfiguration([FromRoute] Guid pluginId) + { + if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + { + return NotFound(); + } + + return plugin.Configuration; + } + + /// + /// Updates plugin configuration. + /// + /// + /// Accepts plugin configuration as JSON body. + /// + /// Plugin id. + /// Plugin configuration updated. + /// Plugin not found or plugin does not have configuration. + /// + /// A that represents the asynchronous operation to update plugin configuration. + /// The task result contains an indicating success, or + /// when plugin not found or plugin doesn't have configuration. + /// + [HttpPost("{pluginId}/Configuration")] + public async Task UpdatePluginConfiguration([FromRoute] Guid pluginId) + { + if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + { + return NotFound(); + } + + var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType) + .ConfigureAwait(false); + + plugin.UpdateConfiguration(configuration); + return Ok(); + } + + /// + /// Get plugin security info. + /// + /// Plugin security info returned. + /// Plugin security info. + [Obsolete("This endpoint should not be used.")] + [HttpGet("SecurityInfo")] + public ActionResult GetPluginSecurityInfo() + { + return new PluginSecurityInfo + { + IsMbSupporter = true, + SupporterKey = "IAmTotallyLegit" + }; + } + + /// + /// Updates plugin security info. + /// + /// Plugin security info. + /// Plugin security info updated. + /// An . + [Obsolete("This endpoint should not be used.")] + [HttpPost("SecurityInfo")] + [Authorize(Policy = Policies.RequiresElevation)] + public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo) + { + return Ok(); + } + + /// + /// Gets registration status for a feature. + /// + /// Feature name. + /// Registration status returned. + /// Mb registration record. + [Obsolete("This endpoint should not be used.")] + [HttpPost("RegistrationRecords/{name}")] + public ActionResult GetRegistrationStatus([FromRoute] string name) + { + return new MBRegistrationRecord + { + IsRegistered = true, + RegChecked = true, + TrialVersion = false, + IsValid = true, + RegError = false + }; + } + + /// + /// Gets registration status for a feature. + /// + /// Feature name. + /// Not implemented. + /// Not Implemented. + /// This endpoint is not implemented. + [Obsolete("Paid plugins are not supported")] + [HttpGet("/Registrations/{name}")] + public ActionResult GetRegistration([FromRoute] string name) + { + // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, + // delete all these registration endpoints. They are only kept for compatibility. + throw new NotImplementedException(); + } + } +} diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs new file mode 100644 index 0000000000..aaaf54267a --- /dev/null +++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs @@ -0,0 +1,42 @@ +#nullable enable + +using System; + +namespace Jellyfin.Api.Models.PluginDtos +{ + /// + /// MB Registration Record. + /// + public class MBRegistrationRecord + { + /// + /// Gets or sets expiration date. + /// + public DateTime ExpirationDate { get; set; } + + /// + /// Gets or sets a value indicating whether is registered. + /// + public bool IsRegistered { get; set; } + + /// + /// Gets or sets a value indicating whether reg checked. + /// + public bool RegChecked { get; set; } + + /// + /// Gets or sets a value indicating whether reg error. + /// + public bool RegError { get; set; } + + /// + /// Gets or sets a value indicating whether trial version. + /// + public bool TrialVersion { get; set; } + + /// + /// Gets or sets a value indicating whether is valid. + /// + public bool IsValid { get; set; } + } +} diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs new file mode 100644 index 0000000000..793002a6cd --- /dev/null +++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Jellyfin.Api.Models.PluginDtos +{ + /// + /// Plugin security info. + /// + public class PluginSecurityInfo + { + /// + /// Gets or sets the supporter key. + /// + public string? SupporterKey { get; set; } + + /// + /// Gets or sets a value indicating whether is mb supporter. + /// + public bool IsMbSupporter { get; set; } + } +} diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs deleted file mode 100644 index 7f74511eec..0000000000 --- a/MediaBrowser.Api/PluginService.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class Plugins - /// - [Route("/Plugins", "GET", Summary = "Gets a list of currently installed plugins")] - [Authenticated] - public class GetPlugins : IReturn - { - public bool? IsAppStoreEnabled { get; set; } - } - - /// - /// Class UninstallPlugin - /// - [Route("/Plugins/{Id}", "DELETE", Summary = "Uninstalls a plugin")] - [Authenticated(Roles = "Admin")] - public class UninstallPlugin : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - /// - /// Class GetPluginConfiguration - /// - [Route("/Plugins/{Id}/Configuration", "GET", Summary = "Gets a plugin's configuration")] - [Authenticated] - public class GetPluginConfiguration - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - /// - /// Class UpdatePluginConfiguration - /// - [Route("/Plugins/{Id}/Configuration", "POST", Summary = "Updates a plugin's configuration")] - [Authenticated] - public class UpdatePluginConfiguration : IRequiresRequestStream, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// The raw Http Request Input Stream - /// - /// The request stream. - public Stream RequestStream { get; set; } - } - - //TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, - // delete all these registration endpoints. They are only kept for compatibility. - [Route("/Registrations/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)] - [Authenticated] - public class GetRegistration : IReturn - { - [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - } - - /// - /// Class GetPluginSecurityInfo - /// - [Route("/Plugins/SecurityInfo", "GET", Summary = "Gets plugin registration information", IsHidden = true)] - [Authenticated] - public class GetPluginSecurityInfo : IReturn - { - } - - /// - /// Class UpdatePluginSecurityInfo - /// - [Route("/Plugins/SecurityInfo", "POST", Summary = "Updates plugin registration information", IsHidden = true)] - [Authenticated(Roles = "Admin")] - public class UpdatePluginSecurityInfo : PluginSecurityInfo, IReturnVoid - { - } - - [Route("/Plugins/RegistrationRecords/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)] - [Authenticated] - public class GetRegistrationStatus - { - [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - } - - // TODO these two classes are only kept for compability with paid plugins and should be removed - public class RegistrationInfo - { - public string Name { get; set; } - public DateTime ExpirationDate { get; set; } - public bool IsTrial { get; set; } - public bool IsRegistered { get; set; } - } - - public class MBRegistrationRecord - { - public DateTime ExpirationDate { get; set; } - public bool IsRegistered { get; set; } - public bool RegChecked { get; set; } - public bool RegError { get; set; } - public bool TrialVersion { get; set; } - public bool IsValid { get; set; } - } - - public class PluginSecurityInfo - { - public string SupporterKey { get; set; } - public bool IsMBSupporter { get; set; } - } - /// - /// Class PluginsService - /// - public class PluginService : BaseApiService - { - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// The _app host - /// - private readonly IApplicationHost _appHost; - private readonly IInstallationManager _installationManager; - - public PluginService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IJsonSerializer jsonSerializer, - IApplicationHost appHost, - IInstallationManager installationManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _appHost = appHost; - _installationManager = installationManager; - _jsonSerializer = jsonSerializer; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetRegistrationStatus request) - { - var record = new MBRegistrationRecord - { - IsRegistered = true, - RegChecked = true, - TrialVersion = false, - IsValid = true, - RegError = false - }; - - return ToOptimizedResult(record); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPlugins request) - { - var result = _appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToArray(); - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPluginConfiguration request) - { - var guid = new Guid(request.Id); - var plugin = _appHost.Plugins.First(p => p.Id == guid) as IHasPluginConfiguration; - - return ToOptimizedResult(plugin.Configuration); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPluginSecurityInfo request) - { - var result = new PluginSecurityInfo - { - IsMBSupporter = true, - SupporterKey = "IAmTotallyLegit" - }; - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(UpdatePluginSecurityInfo request) - { - return Task.CompletedTask; - } - - /// - /// Posts the specified request. - /// - /// The request. - public async Task Post(UpdatePluginConfiguration request) - { - // We need to parse this manually because we told service stack not to with IRequiresRequestStream - // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs - var id = Guid.Parse(GetPathValue(1)); - - if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin)) - { - throw new FileNotFoundException(); - } - - var configuration = (await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, plugin.ConfigurationType).ConfigureAwait(false)) as BasePluginConfiguration; - - plugin.UpdateConfiguration(configuration); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(UninstallPlugin request) - { - var guid = new Guid(request.Id); - var plugin = _appHost.Plugins.First(p => p.Id == guid); - - _installationManager.UninstallPlugin(plugin); - } - } -} From dc190e56833235c1b20fa76b8382da80fc62b0b7 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 9 Jun 2020 18:56:17 +0200 Subject: [PATCH 169/724] 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 170/724] 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 b16da095493bc207f4196b8b61cfc768a237a5bc Mon Sep 17 00:00:00 2001 From: David Date: Wed, 10 Jun 2020 15:18:13 +0200 Subject: [PATCH 171/724] Move /System Endpoint to Jellyfin.Api --- Jellyfin.Api/Controllers/SystemController.cs | 222 ++++++++++++++++++ MediaBrowser.Api/System/SystemService.cs | 226 ------------------- 2 files changed, 222 insertions(+), 226 deletions(-) create mode 100644 Jellyfin.Api/Controllers/SystemController.cs delete mode 100644 MediaBrowser.Api/System/SystemService.cs diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs new file mode 100644 index 0000000000..cab6f308f0 --- /dev/null +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The system controller. + /// + [Route("/System")] + public class SystemController : BaseJellyfinApiController + { + private readonly IServerApplicationHost _appHost; + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly INetworkManager _network; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SystemController( + IServerConfigurationManager serverConfigurationManager, + IServerApplicationHost appHost, + IFileSystem fileSystem, + INetworkManager network, + ILogger logger) + { + _appPaths = serverConfigurationManager.ApplicationPaths; + _appHost = appHost; + _fileSystem = fileSystem; + _network = network; + _logger = logger; + } + + /// + /// Gets information about the server. + /// + /// Information retrieved. + /// A with info about the system. + [HttpGet("Info")] + // TODO: Authorize EscapeParentalControl + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetSystemInfo() + { + return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets public information about the server. + /// + /// Information retrieved. + /// A with public info about the system. + [HttpGet("Info/Public")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetPublicSystemInfo() + { + return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Pings the system. + /// + /// Information retrieved. + /// The server name. + [HttpGet("Ping")] + [HttpPost("Ping")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult PingSystem() + { + return _appHost.Name; + } + + /// + /// Restarts the application. + /// + /// Server restarted. + /// No content. Server restarted. + [HttpPost("Restart")] + // TODO: Authorize AllowLocal = true + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RestartApplication() + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + _appHost.Restart(); + }); + return NoContent(); + } + + /// + /// Shuts down the application. + /// + /// Server shut down. + /// No content. Server shut down. + [HttpPost("Shutdown")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult ShutdownApplication() + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + await _appHost.Shutdown().ConfigureAwait(false); + }); + return NoContent(); + } + + /// + /// Gets a list of available server log files. + /// + /// Information retrieved. + /// An array of with the available log files. + [HttpGet("Logs")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetServerLogs() + { + IEnumerable files; + + try + { + files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error getting logs"); + files = Enumerable.Empty(); + } + + var result = files.Select(i => new LogFile + { + DateCreated = _fileSystem.GetCreationTimeUtc(i), + DateModified = _fileSystem.GetLastWriteTimeUtc(i), + Name = i.Name, + Size = i.Length + }) + .OrderByDescending(i => i.DateModified) + .ThenByDescending(i => i.DateCreated) + .ThenBy(i => i.Name) + .ToArray(); + + return result; + } + + /// + /// Gets information about the request endpoint. + /// + /// Information retrieved. + /// with information about the endpoint. + [HttpGet("Endpoint")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetEndpointInfo() + { + return new EndPointInfo + { + IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress), + IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString()) + }; + } + + /// + /// Gets a log file. + /// + /// The name of the log file to get. + /// Log file retrieved. + /// The log file. + [HttpGet("Logs/Log")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetLogFile([FromQuery, Required] string name) + { + var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) + .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); + + // For older files, assume fully static + var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; + + FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); + return File(stream, "text/plain"); + } + + /// + /// Gets wake on lan information. + /// + /// Information retrieved. + /// An with the WakeOnLan infos. + [HttpGet("WakeOnLanInfo")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetWakeOnLanInfo() + { + var result = _appHost.GetWakeOnLanInfo(); + return Ok(result); + } + } +} diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs deleted file mode 100644 index c57cc93d55..0000000000 --- a/MediaBrowser.Api/System/SystemService.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.System -{ - /// - /// Class GetSystemInfo - /// - [Route("/System/Info", "GET", Summary = "Gets information about the server")] - [Authenticated(EscapeParentalControl = true, AllowBeforeStartupWizard = true)] - public class GetSystemInfo : IReturn - { - - } - - [Route("/System/Info/Public", "GET", Summary = "Gets public information about the server")] - public class GetPublicSystemInfo : IReturn - { - - } - - [Route("/System/Ping", "POST")] - [Route("/System/Ping", "GET")] - public class PingSystem : IReturnVoid - { - - } - - /// - /// Class RestartApplication - /// - [Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")] - [Authenticated(Roles = "Admin", AllowLocal = true)] - public class RestartApplication - { - } - - /// - /// This is currently not authenticated because the uninstaller needs to be able to shutdown the server. - /// - [Route("/System/Shutdown", "POST", Summary = "Shuts down the application")] - [Authenticated(Roles = "Admin", AllowLocal = true)] - public class ShutdownApplication - { - } - - [Route("/System/Logs", "GET", Summary = "Gets a list of available server log files")] - [Authenticated(Roles = "Admin")] - public class GetServerLogs : IReturn - { - } - - [Route("/System/Endpoint", "GET", Summary = "Gets information about the request endpoint")] - [Authenticated] - public class GetEndpointInfo : IReturn - { - public string Endpoint { get; set; } - } - - [Route("/System/Logs/Log", "GET", Summary = "Gets a log file")] - [Authenticated(Roles = "Admin")] - public class GetLogFile - { - [ApiMember(Name = "Name", Description = "The log file name.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string Name { get; set; } - } - - [Route("/System/WakeOnLanInfo", "GET", Summary = "Gets wake on lan information")] - [Authenticated] - public class GetWakeOnLanInfo : IReturn - { - - } - - /// - /// Class SystemInfoService - /// - public class SystemService : BaseApiService - { - /// - /// The _app host - /// - private readonly IServerApplicationHost _appHost; - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - private readonly INetworkManager _network; - - /// - /// Initializes a new instance of the class. - /// - /// The app host. - /// The file system. - /// jsonSerializer - public SystemService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IServerApplicationHost appHost, - IFileSystem fileSystem, - INetworkManager network) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _appPaths = serverConfigurationManager.ApplicationPaths; - _appHost = appHost; - _fileSystem = fileSystem; - _network = network; - } - - public object Post(PingSystem request) - { - return _appHost.Name; - } - - public object Get(GetWakeOnLanInfo request) - { - var result = _appHost.GetWakeOnLanInfo(); - - return ToOptimizedResult(result); - } - - public object Get(GetServerLogs request) - { - IEnumerable files; - - try - { - files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); - } - catch (IOException ex) - { - Logger.LogError(ex, "Error getting logs"); - files = Enumerable.Empty(); - } - - var result = files.Select(i => new LogFile - { - DateCreated = _fileSystem.GetCreationTimeUtc(i), - DateModified = _fileSystem.GetLastWriteTimeUtc(i), - Name = i.Name, - Size = i.Length - - }).OrderByDescending(i => i.DateModified) - .ThenByDescending(i => i.DateCreated) - .ThenBy(i => i.Name) - .ToArray(); - - return ToOptimizedResult(result); - } - - public Task Get(GetLogFile request) - { - var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) - .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase)); - - // For older files, assume fully static - var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; - - return ResultFactory.GetStaticFileResult(Request, file.FullName, fileShare); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task Get(GetSystemInfo request) - { - var result = await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Get(GetPublicSystemInfo request) - { - var result = await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(RestartApplication request) - { - _appHost.Restart(); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(ShutdownApplication request) - { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - await _appHost.Shutdown().ConfigureAwait(false); - }); - } - - public object Get(GetEndpointInfo request) - { - return ToOptimizedResult(new EndPointInfo - { - IsLocal = Request.IsLocal, - IsInNetwork = _network.IsInLocalNetwork(request.Endpoint ?? Request.RemoteIp) - }); - } - } -} From 6a70081643de80a2053b5903644cdcfa4bcbfc61 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 10 Jun 2020 15:57:31 +0200 Subject: [PATCH 172/724] 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; - } - } -} From 393f5f0c2581a19abf4edf500802f9556117ce7a Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Wed, 10 Jun 2020 09:23:20 -0600 Subject: [PATCH 173/724] Update Jellyfin.Api/Controllers/FilterController.cs Co-authored-by: Patrick Barron <18354464+barronpm@users.noreply.github.com> --- Jellyfin.Api/Controllers/FilterController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index d06c5e96c9..0f6124714f 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -195,10 +195,10 @@ namespace Jellyfin.Api.Controllers genreQuery.Parent = parentItem; } - if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase)) { filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair { @@ -218,4 +218,4 @@ namespace Jellyfin.Api.Controllers return filters; } } -} \ No newline at end of file +} From 355682620d120ded27c33b528e554982946de86c Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Wed, 10 Jun 2020 09:23:27 -0600 Subject: [PATCH 174/724] Update Jellyfin.Api/Controllers/FilterController.cs Co-authored-by: Patrick Barron <18354464+barronpm@users.noreply.github.com> --- Jellyfin.Api/Controllers/FilterController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 0f6124714f..431114ea9a 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -62,10 +62,10 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); - if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) { parentItem = null; } From f64e8e8757c01d45779f90a38e1bb0033c197353 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 10 Jun 2020 18:20:54 +0200 Subject: [PATCH 175/724] Move SubtitleService to Jellyfin.Api --- .../Controllers/SubtitleController.cs | 344 ++++++++++++++++++ MediaBrowser.Api/Subtitles/SubtitleService.cs | 300 --------------- 2 files changed, 344 insertions(+), 300 deletions(-) create mode 100644 Jellyfin.Api/Controllers/SubtitleController.cs delete mode 100644 MediaBrowser.Api/Subtitles/SubtitleService.cs diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs new file mode 100644 index 0000000000..fe5e133386 --- /dev/null +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -0,0 +1,344 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Subtitle controller. + /// + public class SubtitleController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly ISubtitleManager _subtitleManager; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + private readonly IAuthorizationContext _authContext; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SubtitleController( + ILibraryManager libraryManager, + ISubtitleManager subtitleManager, + ISubtitleEncoder subtitleEncoder, + IMediaSourceManager mediaSourceManager, + IProviderManager providerManager, + IFileSystem fileSystem, + IAuthorizationContext authContext, + ILogger logger) + { + _libraryManager = libraryManager; + _subtitleManager = subtitleManager; + _subtitleEncoder = subtitleEncoder; + _mediaSourceManager = mediaSourceManager; + _providerManager = providerManager; + _fileSystem = fileSystem; + _authContext = authContext; + _logger = logger; + } + + /// + /// Deletes an external subtitle file. + /// + /// The item id. + /// The index of the subtitle file. + /// Subtitle deleted. + /// Item not found. + /// A . + [HttpDelete("/Videos/{id}/Subtitles/{index}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteSubtitle( + [FromRoute] Guid id, + [FromRoute] int index) + { + var item = _libraryManager.GetItemById(id); + + if (item == null) + { + return NotFound(); + } + + _subtitleManager.DeleteSubtitles(item, index); + return NoContent(); + } + + /// + /// Search remote subtitles. + /// + /// The item id. + /// The language of the subtitles. + /// Optional. Only show subtitles which are a perfect match. + /// Subtitles retrieved. + /// An array of . + [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> SearchRemoteSubtitles( + [FromRoute] Guid id, + [FromRoute] string language, + [FromQuery] bool isPerfectMatch) + { + var video = (Video)_libraryManager.GetItemById(id); + + return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Downloads a remote subtitle. + /// + /// The item id. + /// The subtitle id. + /// Subtitle downloaded. + /// A . + [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DownloadRemoteSubtitles( + [FromRoute] Guid id, + [FromRoute] string subtitleId) + { + var video = (Video)_libraryManager.GetItemById(id); + + Task.Run(async () => + { + try + { + await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None) + .ConfigureAwait(false); + + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading subtitles"); + } + }); + return NoContent(); + } + + /// + /// Gets the remote subtitles. + /// + /// The item id. + /// File returned. + /// A with the subtitle file. + [HttpGet("/Providers/Subtitles/Subtitles/{id}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetRemoteSubtitles([FromRoute] string id) + { + var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); + + return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format)); + } + + /// + /// Gets subtitles in a specified format. + /// + /// The item id. + /// The media source id. + /// The subtitle stream index. + /// The format of the returned subtitle. + /// Optional. The start position of the subtitle in ticks. + /// Optional. The end position of the subtitle in ticks. + /// Optional. Whether to copy the timestamps. + /// Optional. Whether to add a VTT time map. + /// File returned. + /// A with the subtitle file. + [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}")] + [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetSubtitle( + [FromRoute, Required] Guid id, + [FromRoute, Required] string mediaSourceId, + [FromRoute, Required] int index, + [FromRoute, Required] string format, + [FromRoute] long startPositionTicks, + [FromQuery] long? endPositionTicks, + [FromQuery] bool copyTimestamps, + [FromQuery] bool addVttTimeMap) + { + if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) + { + format = "json"; + } + + if (string.IsNullOrEmpty(format)) + { + var item = (Video)_libraryManager.GetItemById(id); + + var idString = id.ToString("N", CultureInfo.InvariantCulture); + var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) + .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); + + var subtitleStream = mediaSource.MediaStreams + .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index); + + FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read); + return File(stream, MimeTypes.GetMimeType(subtitleStream.Path)); + } + + if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) + { + using var stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + using var reader = new StreamReader(stream); + + var text = reader.ReadToEnd(); + + text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); + + return File(text, MimeTypes.GetMimeType("file." + format)); + } + + return File( + await EncodeSubtitles( + id, + mediaSourceId, + index, + format, + startPositionTicks, + endPositionTicks, + copyTimestamps).ConfigureAwait(false), + MimeTypes.GetMimeType("file." + format)); + } + + /// + /// Gets an HLS subtitle playlist. + /// + /// The item id. + /// The subtitle stream index. + /// The media source id. + /// The subtitle segment length. + /// Subtitle playlist retrieved. + /// A with the HLS subtitle playlist. + [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetSubtitlePlaylist( + [FromRoute] Guid id, + [FromRoute] int index, + [FromRoute] string mediaSourceId, + [FromQuery, Required] int segmentLength) + { + var item = (Video)_libraryManager.GetItemById(id); + + var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false); + + var builder = new StringBuilder(); + + var runtime = mediaSource.RunTimeTicks ?? -1; + + if (runtime <= 0) + { + throw new ArgumentException("HLS Subtitles are not supported for this media."); + } + + var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks; + if (segmentLengthTicks <= 0) + { + throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)"); + } + + builder.AppendLine("#EXTM3U"); + builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture)); + builder.AppendLine("#EXT-X-VERSION:3"); + builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); + builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); + + long positionTicks = 0; + + var accessToken = _authContext.GetAuthorizationInfo(Request).Token; + + while (positionTicks < runtime) + { + var remaining = runtime - positionTicks; + var lengthTicks = Math.Min(remaining, segmentLengthTicks); + + builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ","); + + var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); + + var url = string.Format( + CultureInfo.CurrentCulture, + "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", + positionTicks.ToString(CultureInfo.InvariantCulture), + endPositionTicks.ToString(CultureInfo.InvariantCulture), + accessToken); + + builder.AppendLine(url); + + positionTicks += segmentLengthTicks; + } + + builder.AppendLine("#EXT-X-ENDLIST"); + return File(builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8")); + } + + /// + /// Encodes a subtitle in the specified format. + /// + /// The media id. + /// The source media id. + /// The subtitle index. + /// The format to convert to. + /// The start position in ticks. + /// The end position in ticks. + /// Whether to copy the timestamps. + /// A with the new subtitle file. + private Task EncodeSubtitles( + Guid id, + string mediaSourceId, + int index, + string format, + long startPositionTicks, + long? endPositionTicks, + bool copyTimestamps) + { + var item = _libraryManager.GetItemById(id); + + return _subtitleEncoder.GetSubtitles( + item, + mediaSourceId, + index, + format, + startPositionTicks, + endPositionTicks ?? 0, + copyTimestamps, + CancellationToken.None); + } + } +} diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs deleted file mode 100644 index f2968c6b5c..0000000000 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ /dev/null @@ -1,300 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace MediaBrowser.Api.Subtitles -{ - [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")] - [Authenticated(Roles = "Admin")] - public class DeleteSubtitle - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - - [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")] - public int Index { get; set; } - } - - [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")] - [Authenticated] - public class SearchRemoteSubtitles : IReturn - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - - [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Language { get; set; } - - public bool? IsPerfectMatch { get; set; } - } - - [Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")] - [Authenticated] - public class DownloadRemoteSubtitles : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - - [ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SubtitleId { get; set; } - } - - [Route("/Providers/Subtitles/Subtitles/{Id}", "GET")] - [Authenticated] - public class GetRemoteSubtitles : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")] - [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")] - public class GetSubtitle - { - /// - /// Gets or sets the id. - /// - /// The id. - [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 subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] - public int Index { get; set; } - - [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Format { get; set; } - - [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public long StartPositionTicks { get; set; } - - [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public long? EndPositionTicks { get; set; } - - [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool CopyTimestamps { get; set; } - public bool AddVttTimeMap { get; set; } - } - - [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")] - [Authenticated] - public class GetSubtitlePlaylist - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string 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 subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] - public int Index { get; set; } - - [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")] - public int SegmentLength { get; set; } - } - - public class SubtitleService : BaseApiService - { - private readonly ILibraryManager _libraryManager; - private readonly ISubtitleManager _subtitleManager; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - private readonly IAuthorizationContext _authContext; - - public SubtitleService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - ISubtitleManager subtitleManager, - ISubtitleEncoder subtitleEncoder, - IMediaSourceManager mediaSourceManager, - IProviderManager providerManager, - IFileSystem fileSystem, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _libraryManager = libraryManager; - _subtitleManager = subtitleManager; - _subtitleEncoder = subtitleEncoder; - _mediaSourceManager = mediaSourceManager; - _providerManager = providerManager; - _fileSystem = fileSystem; - _authContext = authContext; - } - - public async Task Get(GetSubtitlePlaylist request) - { - var item = (Video)_libraryManager.GetItemById(new Guid(request.Id)); - - var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false); - - var builder = new StringBuilder(); - - var runtime = mediaSource.RunTimeTicks ?? -1; - - if (runtime <= 0) - { - throw new ArgumentException("HLS Subtitles are not supported for this media."); - } - - var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks; - if (segmentLengthTicks <= 0) - { - throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)"); - } - - builder.AppendLine("#EXTM3U"); - builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture)); - builder.AppendLine("#EXT-X-VERSION:3"); - builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); - builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); - - long positionTicks = 0; - - var accessToken = _authContext.GetAuthorizationInfo(Request).Token; - - while (positionTicks < runtime) - { - var remaining = runtime - positionTicks; - var lengthTicks = Math.Min(remaining, segmentLengthTicks); - - builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ","); - - var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); - - var url = string.Format("stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", - positionTicks.ToString(CultureInfo.InvariantCulture), - endPositionTicks.ToString(CultureInfo.InvariantCulture), - accessToken); - - builder.AppendLine(url); - - positionTicks += segmentLengthTicks; - } - - builder.AppendLine("#EXT-X-ENDLIST"); - - return ResultFactory.GetResult(Request, builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); - } - - public async Task Get(GetSubtitle request) - { - if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase)) - { - request.Format = "json"; - } - if (string.IsNullOrEmpty(request.Format)) - { - var item = (Video)_libraryManager.GetItemById(request.Id); - - var idString = request.Id.ToString("N", CultureInfo.InvariantCulture); - var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false, null) - .First(i => string.Equals(i.Id, request.MediaSourceId ?? idString)); - - var subtitleStream = mediaSource.MediaStreams - .First(i => i.Type == MediaStreamType.Subtitle && i.Index == request.Index); - - return await ResultFactory.GetStaticFileResult(Request, subtitleStream.Path).ConfigureAwait(false); - } - - if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap) - { - using var stream = await GetSubtitles(request).ConfigureAwait(false); - using var reader = new StreamReader(stream); - - var text = reader.ReadToEnd(); - - text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000"); - - return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format)); - } - - return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format)); - } - - private Task GetSubtitles(GetSubtitle request) - { - var item = _libraryManager.GetItemById(request.Id); - - return _subtitleEncoder.GetSubtitles(item, - request.MediaSourceId, - request.Index, - request.Format, - request.StartPositionTicks, - request.EndPositionTicks ?? 0, - request.CopyTimestamps, - CancellationToken.None); - } - - public async Task Get(SearchRemoteSubtitles request) - { - var video = (Video)_libraryManager.GetItemById(request.Id); - - return await _subtitleManager.SearchSubtitles(video, request.Language, request.IsPerfectMatch, CancellationToken.None).ConfigureAwait(false); - } - - public Task Delete(DeleteSubtitle request) - { - var item = _libraryManager.GetItemById(request.Id); - return _subtitleManager.DeleteSubtitles(item, request.Index); - } - - public async Task Get(GetRemoteSubtitles request) - { - var result = await _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).ConfigureAwait(false); - - return ResultFactory.GetResult(Request, result.Stream, MimeTypes.GetMimeType("file." + result.Format)); - } - - public void Post(DownloadRemoteSubtitles request) - { - var video = (Video)_libraryManager.GetItemById(request.Id); - - Task.Run(async () => - { - try - { - await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None) - .ConfigureAwait(false); - - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error downloading subtitles"); - } - }); - } - } -} From 8178b194708f4added870597500a88d0ba5a3cfa Mon Sep 17 00:00:00 2001 From: David Date: Thu, 11 Jun 2020 12:29:56 +0200 Subject: [PATCH 176/724] Fix suggestions --- .../Controllers/SubtitleController.cs | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index fe5e133386..15c7d1eaad 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -1,8 +1,12 @@ -using System; +#nullable enable + +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -107,10 +111,10 @@ namespace Jellyfin.Api.Controllers [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> SearchRemoteSubtitles( + public async Task>> SearchRemoteSubtitles( [FromRoute] Guid id, [FromRoute] string language, - [FromQuery] bool isPerfectMatch) + [FromQuery] bool? isPerfectMatch) { var video = (Video)_libraryManager.GetItemById(id); @@ -127,26 +131,24 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult DownloadRemoteSubtitles( + public async Task DownloadRemoteSubtitles( [FromRoute] Guid id, [FromRoute] string subtitleId) { var video = (Video)_libraryManager.GetItemById(id); - Task.Run(async () => + try { - try - { - await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None) - .ConfigureAwait(false); + await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None) + .ConfigureAwait(false); + + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading subtitles"); + } - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error downloading subtitles"); - } - }); return NoContent(); } @@ -159,6 +161,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("/Providers/Subtitles/Subtitles/{id}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [Produces(MediaTypeNames.Application.Octet)] public async Task GetRemoteSubtitles([FromRoute] string id) { var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); @@ -179,8 +182,8 @@ namespace Jellyfin.Api.Controllers /// Optional. Whether to add a VTT time map. /// File returned. /// A with the subtitle file. - [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}")] - [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}")] + [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] + [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitle( [FromRoute, Required] Guid id, @@ -217,11 +220,11 @@ namespace Jellyfin.Api.Controllers using var stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); using var reader = new StreamReader(stream); - var text = reader.ReadToEnd(); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); - return File(text, MimeTypes.GetMimeType("file." + format)); + return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format)); } return File( @@ -305,7 +308,7 @@ namespace Jellyfin.Api.Controllers } builder.AppendLine("#EXT-X-ENDLIST"); - return File(builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8")); + return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); } /// From fcbae95d1945ad5d632c5c86253c02da657db339 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 11 Jun 2020 15:57:31 +0200 Subject: [PATCH 177/724] Use 'await using Stream' instead of 'using Stream' --- Jellyfin.Api/Controllers/SubtitleController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 15c7d1eaad..ba2250d81a 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -217,7 +217,7 @@ namespace Jellyfin.Api.Controllers if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) { - using var stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + await using Stream stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); using var reader = new StreamReader(stream); var text = await reader.ReadToEndAsync().ConfigureAwait(false); From a47ff4043f2116716d5f15d1f79657550052bde8 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 11 Jun 2020 16:01:41 +0200 Subject: [PATCH 178/724] Disable CA1801 --- Jellyfin.Api/Controllers/SubtitleController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index ba2250d81a..97df8c60d8 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -1,4 +1,5 @@ #nullable enable +#pragma warning disable CA1801 using System; using System.Collections.Generic; @@ -253,6 +254,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitlePlaylist( [FromRoute] Guid id, + // TODO: 'int index' is never used: CA1801 is disabled [FromRoute] int index, [FromRoute] string mediaSourceId, [FromQuery, Required] int segmentLength) From 043d76bd6e9e4a2e1093ae0e5ba025de1438b528 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 12 Jun 2020 12:38:13 +0200 Subject: [PATCH 179/724] Use Http status code 204 instead of 200 --- .../Controllers/ConfigurationController.cs | 18 +++++------ Jellyfin.Api/Controllers/DevicesController.cs | 16 +++++----- .../Controllers/NotificationsController.cs | 24 +++++++------- Jellyfin.Api/Controllers/PackageController.cs | 15 +++++---- Jellyfin.Api/Controllers/StartupController.cs | 32 +++++++++---------- 5 files changed, 53 insertions(+), 52 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 2a1dce74d4..780a38aa81 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -53,15 +53,15 @@ namespace Jellyfin.Api.Controllers /// Updates application configuration. /// /// Configuration. - /// Configuration updated. + /// Configuration updated. /// Update status. [HttpPost("Configuration")] [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) { _configurationManager.ReplaceConfiguration(configuration); - return Ok(); + return NoContent(); } /// @@ -81,17 +81,17 @@ namespace Jellyfin.Api.Controllers /// Updates named configuration. /// /// Configuration key. - /// Named configuration updated. + /// Named configuration updated. /// Update status. [HttpPost("Configuration/{Key}")] [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UpdateNamedConfiguration([FromRoute] string key) { var configurationType = _configurationManager.GetConfigurationType(key); var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false); _configurationManager.SaveConfiguration(key, configuration); - return Ok(); + return NoContent(); } /// @@ -111,15 +111,15 @@ namespace Jellyfin.Api.Controllers /// Updates the path to the media encoder. /// /// Media encoder path form body. - /// Media encoder path updated. + /// Media encoder path updated. /// Status. [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) { _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); - return Ok(); + return NoContent(); } } } diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 1e75579033..1754b0cbda 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -105,12 +105,12 @@ namespace Jellyfin.Api.Controllers /// /// Device Id. /// Device Options. - /// Device options updated. + /// Device options updated. /// Device not found. - /// An on success, or a if the device could not be found. + /// A on success, or a if the device could not be found. [HttpPost("Options")] [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( [FromQuery, BindRequired] string id, @@ -123,18 +123,18 @@ namespace Jellyfin.Api.Controllers } _deviceManager.UpdateDeviceOptions(id, deviceOptions); - return Ok(); + return NoContent(); } /// /// Deletes a device. /// /// Device Id. - /// Device deleted. + /// Device deleted. /// Device not found. - /// An on success, or a if the device could not be found. + /// A on success, or a if the device could not be found. [HttpDelete] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteDevice([FromQuery, BindRequired] string id) { var existingDevice = _deviceManager.GetDevice(id); @@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.Logout(session); } - return Ok(); + return NoContent(); } } } diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 8d82ca10f1..5af1947562 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -99,10 +99,10 @@ namespace Jellyfin.Api.Controllers /// The description of the notification. /// The URL of the notification. /// The level of the notification. - /// Notification sent. - /// An . + /// Notification sent. + /// A . [HttpPost("Admin")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, @@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers _notificationManager.SendNotification(notification, CancellationToken.None); - return Ok(); + return NoContent(); } /// @@ -129,15 +129,15 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. - /// Notifications set as read. - /// An . + /// Notifications set as read. + /// A . [HttpPost("{UserID}/Read")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRead( [FromRoute] string userId, [FromQuery] string ids) { - return Ok(); + return NoContent(); } /// @@ -145,15 +145,15 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. - /// Notifications set as unread. - /// An . + /// Notifications set as unread. + /// A . [HttpPost("{UserID}/Unread")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetUnread( [FromRoute] string userId, [FromQuery] string ids) { - return Ok(); + return NoContent(); } } } diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index f37319c19e..8200f891c8 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -72,11 +72,11 @@ namespace Jellyfin.Api.Controllers /// Package name. /// GUID of the associated assembly. /// Optional version. Defaults to latest version. - /// Package found. + /// Package found. /// Package not found. - /// An on success, or a if the package could not be found. + /// A on success, or a if the package could not be found. [HttpPost("/Installed/{Name}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = Policies.RequiresElevation)] public async Task InstallPackage( @@ -98,23 +98,24 @@ namespace Jellyfin.Api.Controllers await _installationManager.InstallPackage(package).ConfigureAwait(false); - return Ok(); + return NoContent(); } /// /// Cancels a package installation. /// /// Installation Id. - /// Installation cancelled. - /// An on successfully cancelling a package installation. + /// Installation cancelled. + /// A on successfully cancelling a package installation. [HttpDelete("/Installing/{id}")] [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public IActionResult CancelPackageInstallation( [FromRoute] [Required] string id) { _installationManager.CancelInstallation(new Guid(id)); - return Ok(); + return NoContent(); } } } diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 57a02e62a9..aae066e0e1 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -33,16 +33,16 @@ namespace Jellyfin.Api.Controllers /// /// Completes the startup wizard. /// - /// Startup wizard completed. - /// An indicating success. + /// Startup wizard completed. + /// A indicating success. [HttpPost("Complete")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); _config.SaveConfiguration(); - return Ok(); + return NoContent(); } /// @@ -70,10 +70,10 @@ namespace Jellyfin.Api.Controllers /// The UI language culture. /// The metadata country code. /// The preferred language for metadata. - /// Configuration saved. - /// An indicating success. + /// Configuration saved. + /// A indicating success. [HttpPost("Configuration")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, @@ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers _config.Configuration.MetadataCountryCode = metadataCountryCode; _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; _config.SaveConfiguration(); - return Ok(); + return NoContent(); } /// @@ -91,16 +91,16 @@ namespace Jellyfin.Api.Controllers /// /// Enable remote access. /// Enable UPnP. - /// Configuration saved. - /// An indicating success. + /// Configuration saved. + /// A indicating success. [HttpPost("RemoteAccess")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; _config.SaveConfiguration(); - return Ok(); + return NoContent(); } /// @@ -121,13 +121,13 @@ namespace Jellyfin.Api.Controllers /// Sets the user name and password. /// /// The DTO containing username and password. - /// Updated user name and password. + /// Updated user name and password. /// /// A that represents the asynchronous update operation. - /// The task result contains an indicating success. + /// The task result contains a indicating success. /// [HttpPost("User")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); @@ -141,7 +141,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); } - return Ok(); + return NoContent(); } } } From 618b893c481a658820370cdf4f62c5a9a2deebd1 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 12 Jun 2020 15:39:06 +0200 Subject: [PATCH 180/724] Move LibraryStructureService to Jellyfin.Api --- .../Controllers/LibraryStructureController.cs | 347 +++++++++++++++ .../Library/LibraryStructureService.cs | 412 ------------------ 2 files changed, 347 insertions(+), 412 deletions(-) create mode 100644 Jellyfin.Api/Controllers/LibraryStructureController.cs delete mode 100644 MediaBrowser.Api/Library/LibraryStructureService.cs diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs new file mode 100644 index 0000000000..f074a61dbe --- /dev/null +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -0,0 +1,347 @@ +#pragma warning disable CA1801 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The library structure controller. + /// + [Route("/Library/VirtualFolders")] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + public class LibraryStructureController : BaseJellyfinApiController + { + private readonly IServerApplicationPaths _appPaths; + private readonly ILibraryManager _libraryManager; + private readonly ILibraryMonitor _libraryMonitor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public LibraryStructureController( + IServerConfigurationManager serverConfigurationManager, + ILibraryManager libraryManager, + ILibraryMonitor libraryMonitor) + { + _appPaths = serverConfigurationManager?.ApplicationPaths; + _libraryManager = libraryManager; + _libraryMonitor = libraryMonitor; + } + + /// + /// Gets all virtual folders. + /// + /// The user id. + /// Virtual folders retrieved. + /// An with the virtual folders. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetVirtualFolders([FromQuery] string userId) + { + return _libraryManager.GetVirtualFolders(true); + } + + /// + /// Adds a virtual folder. + /// + /// The name of the virtual folder. + /// The type of the collection. + /// Whether to refresh the library. + /// The paths of the virtual folder. + /// The library options. + /// Folder added. + /// A . + [HttpPost] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddVirtualFolder( + [FromQuery] string name, + [FromQuery] string collectionType, + [FromQuery] bool refreshLibrary, + [FromQuery] string[] paths, + [FromQuery] LibraryOptions libraryOptions) + { + libraryOptions ??= new LibraryOptions(); + + if (paths != null && paths.Length > 0) + { + libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); + } + + _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary); + + return NoContent(); + } + + /// + /// Removes a virtual folder. + /// + /// The name of the folder. + /// Whether to refresh the library. + /// Folder removed. + /// A . + [HttpDelete] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveVirtualFolder( + [FromQuery] string name, + [FromQuery] bool refreshLibrary) + { + _libraryManager.RemoveVirtualFolder(name, refreshLibrary); + return NoContent(); + } + + /// + /// Renames a virtual folder. + /// + /// The name of the virtual folder. + /// The new name. + /// Whether to refresh the library. + /// Folder renamed. + /// Library doesn't exist. + /// Library already exists. + /// A on success, a if the library doesn't exist, a if the new name is already taken. + /// The new name may not be null. + [HttpPost("Name")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public ActionResult RenameVirtualFolder( + [FromQuery] string name, + [FromQuery] string newName, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(newName)) + { + throw new ArgumentNullException(nameof(newName)); + } + + var rootFolderPath = _appPaths.DefaultUserViewsPath; + + var currentPath = Path.Combine(rootFolderPath, name); + var newPath = Path.Combine(rootFolderPath, newName); + + if (!Directory.Exists(currentPath)) + { + return NotFound("The media collection does not exist."); + } + + if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) + { + return Conflict($"The media library already exists at {newPath}."); + } + + _libraryMonitor.Stop(); + + try + { + // Changing capitalization. Handle windows case insensitivity + if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) + { + var tempPath = Path.Combine( + rootFolderPath, + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + Directory.Move(currentPath, tempPath); + currentPath = tempPath; + } + + Directory.Move(currentPath, newPath); + } + finally + { + CollectionFolder.OnCollectionFolderChange(); + + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// + /// Add a media path to a library. + /// + /// The name of the library. + /// The path to add. + /// The path info. + /// Whether to refresh the library. + /// A . + /// Media path added. + /// The name of the library may not be empty. + [HttpPost("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddMediaPath( + [FromQuery] string name, + [FromQuery] string path, + [FromQuery] MediaPathInfo pathInfo, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryMonitor.Stop(); + + try + { + var mediaPath = pathInfo ?? new MediaPathInfo { Path = path }; + + _libraryManager.AddMediaPath(name, mediaPath); + } + finally + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// + /// Updates a media path. + /// + /// The name of the library. + /// The path info. + /// A . + /// Media path updated. + /// The name of the library may not be empty. + [HttpPost("Paths/Update")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateMediaPath( + [FromQuery] string name, + [FromQuery] MediaPathInfo pathInfo) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryManager.UpdateMediaPath(name, pathInfo); + return NoContent(); + } + + /// + /// Remove a media path. + /// + /// The name of the library. + /// The path to remove. + /// Whether to refresh the library. + /// A . + /// Media path removed. + /// The name of the library may not be empty. + [HttpDelete("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveMediaPath( + [FromQuery] string name, + [FromQuery] string path, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryMonitor.Stop(); + + try + { + _libraryManager.RemoveMediaPath(name, path); + } + finally + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// + /// Update library options. + /// + /// The library name. + /// The library options. + /// Library updated. + /// A . + [HttpPost("LibraryOptions")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateLibraryOptions( + [FromQuery] string id, + [FromQuery] LibraryOptions libraryOptions) + { + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id); + + collectionFolder.UpdateLibraryOptions(libraryOptions); + return NoContent(); + } + } +} diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs deleted file mode 100644 index 1e300814f6..0000000000 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ /dev/null @@ -1,412 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Library -{ - /// - /// Class GetDefaultVirtualFolders - /// - [Route("/Library/VirtualFolders", "GET")] - public class GetVirtualFolders : IReturn> - { - /// - /// Gets or sets the user id. - /// - /// The user id. - public string UserId { get; set; } - } - - [Route("/Library/VirtualFolders", "POST")] - public class AddVirtualFolder : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the type of the collection. - /// - /// The type of the collection. - public string CollectionType { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - - /// - /// Gets or sets the path. - /// - /// The path. - public string[] Paths { get; set; } - - public LibraryOptions LibraryOptions { get; set; } - } - - [Route("/Library/VirtualFolders", "DELETE")] - public class RemoveVirtualFolder : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/Name", "POST")] - public class RenameVirtualFolder : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - public string NewName { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/Paths", "POST")] - public class AddMediaPath : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - public string Path { get; set; } - - public MediaPathInfo PathInfo { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/Paths/Update", "POST")] - public class UpdateMediaPath : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - public MediaPathInfo PathInfo { get; set; } - } - - [Route("/Library/VirtualFolders/Paths", "DELETE")] - public class RemoveMediaPath : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - public string Path { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/LibraryOptions", "POST")] - public class UpdateLibraryOptions : IReturnVoid - { - public string Id { get; set; } - - public LibraryOptions LibraryOptions { get; set; } - } - - /// - /// Class LibraryStructureService - /// - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] - public class LibraryStructureService : BaseApiService - { - /// - /// The _app paths - /// - private readonly IServerApplicationPaths _appPaths; - - /// - /// The _library manager - /// - private readonly ILibraryManager _libraryManager; - private readonly ILibraryMonitor _libraryMonitor; - - - /// - /// Initializes a new instance of the class. - /// - public LibraryStructureService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - ILibraryMonitor libraryMonitor) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _appPaths = serverConfigurationManager.ApplicationPaths; - _libraryManager = libraryManager; - _libraryMonitor = libraryMonitor; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetVirtualFolders request) - { - var result = _libraryManager.GetVirtualFolders(true); - - return ToOptimizedResult(result); - } - - public void Post(UpdateLibraryOptions request) - { - var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); - - collectionFolder.UpdateLibraryOptions(request.LibraryOptions); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(AddVirtualFolder request) - { - var libraryOptions = request.LibraryOptions ?? new LibraryOptions(); - - if (request.Paths != null && request.Paths.Length > 0) - { - libraryOptions.PathInfos = request.Paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); - } - - return _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, libraryOptions, request.RefreshLibrary); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(RenameVirtualFolder request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - if (string.IsNullOrWhiteSpace(request.NewName)) - { - throw new ArgumentNullException(nameof(request)); - } - - var rootFolderPath = _appPaths.DefaultUserViewsPath; - - var currentPath = Path.Combine(rootFolderPath, request.Name); - var newPath = Path.Combine(rootFolderPath, request.NewName); - - if (!Directory.Exists(currentPath)) - { - throw new FileNotFoundException("The media collection does not exist"); - } - - if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) - { - throw new ArgumentException("Media library already exists at " + newPath + "."); - } - - _libraryMonitor.Stop(); - - try - { - // Changing capitalization. Handle windows case insensitivity - if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) - { - var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); - Directory.Move(currentPath, tempPath); - currentPath = tempPath; - } - - Directory.Move(currentPath, newPath); - } - finally - { - CollectionFolder.OnCollectionFolderChange(); - - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } - } - - /// - /// Deletes the specified request. - /// - /// The request. - public Task Delete(RemoveVirtualFolder request) - { - return _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(AddMediaPath request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - _libraryMonitor.Stop(); - - try - { - var mediaPath = request.PathInfo ?? new MediaPathInfo - { - Path = request.Path - }; - - _libraryManager.AddMediaPath(request.Name, mediaPath); - } - finally - { - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(UpdateMediaPath request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - _libraryManager.UpdateMediaPath(request.Name, request.PathInfo); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(RemoveMediaPath request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - _libraryMonitor.Stop(); - - try - { - _libraryManager.RemoveMediaPath(request.Name, request.Path); - } - finally - { - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } - } - } -} From fff3c789b98aad08f8ea66da275ff82c71dd8f2b Mon Sep 17 00:00:00 2001 From: David Date: Fri, 12 Jun 2020 18:54:25 +0200 Subject: [PATCH 181/724] Move SessionService to Jellyfin.Api --- Jellyfin.Api/Controllers/SessionController.cs | 478 +++++++++++++++++ Jellyfin.Api/Helpers/RequestHelpers.cs | 15 + MediaBrowser.Api/Sessions/SessionService.cs | 498 ------------------ 3 files changed, 493 insertions(+), 498 deletions(-) create mode 100644 Jellyfin.Api/Controllers/SessionController.cs delete mode 100644 MediaBrowser.Api/Sessions/SessionService.cs diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs new file mode 100644 index 0000000000..5b60275eb6 --- /dev/null +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -0,0 +1,478 @@ +#pragma warning disable CA1801 + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading; +using Jellyfin.Api.Helpers; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The session controller. + /// + public class SessionController : BaseJellyfinApiController + { + private readonly ISessionManager _sessionManager; + private readonly IUserManager _userManager; + private readonly IAuthorizationContext _authContext; + private readonly IDeviceManager _deviceManager; + private readonly ISessionContext _sessionContext; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SessionController( + ISessionManager sessionManager, + IUserManager userManager, + IAuthorizationContext authContext, + IDeviceManager deviceManager, + ISessionContext sessionContext) + { + _sessionManager = sessionManager; + _userManager = userManager; + _authContext = authContext; + _deviceManager = deviceManager; + _sessionContext = sessionContext; + } + + /// + /// Gets a list of sessions. + /// + /// Filter by sessions that a given user is allowed to remote control. + /// Filter by device Id. + /// Optional. Filter by sessions that were active in the last n seconds. + /// List of sessions returned. + /// An with the available sessions. + [HttpGet("/Sessions")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSessions( + [FromQuery] Guid controllableByUserId, + [FromQuery] string deviceId, + [FromQuery] int? activeWithinSeconds) + { + var result = _sessionManager.Sessions; + + if (!string.IsNullOrEmpty(deviceId)) + { + result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + } + + if (!controllableByUserId.Equals(Guid.Empty)) + { + result = result.Where(i => i.SupportsRemoteControl); + + var user = _userManager.GetUserById(controllableByUserId); + + if (!user.Policy.EnableRemoteControlOfOtherUsers) + { + result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId)); + } + + if (!user.Policy.EnableSharedDeviceControl) + { + result = result.Where(i => !i.UserId.Equals(Guid.Empty)); + } + + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } + + result = result.Where(i => + { + if (!string.IsNullOrWhiteSpace(i.DeviceId)) + { + if (!_deviceManager.CanAccessDevice(user, i.DeviceId)) + { + return false; + } + } + + return true; + }); + } + + return Ok(result); + } + + /// + /// Instructs a session to browse to an item or view. + /// + /// The session Id. + /// The type of item to browse to. + /// The Id of the item. + /// The name of the item. + /// Instruction sent to session. + /// A . + [HttpPost("/Sessions/{id}/Viewing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DisplayContent( + [FromRoute] string id, + [FromQuery] string itemType, + [FromQuery] string itemId, + [FromQuery] string itemName) + { + var command = new BrowseRequest + { + ItemId = itemId, + ItemName = itemName, + ItemType = itemType + }; + + _sessionManager.SendBrowseCommand( + RequestHelpers.GetSession(_sessionContext).Id, + id, + command, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Instructs a session to play an item. + /// + /// The session id. + /// The ids of the items to play, comma delimited. + /// The starting position of the first item. + /// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now. + /// The . + /// Instruction sent to session. + /// A . + [HttpPost("/Sessions/{id}/Playing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Play( + [FromRoute] string id, + [FromQuery] Guid[] itemIds, + [FromQuery] long? startPositionTicks, + [FromQuery] PlayCommand playCommand, + [FromBody, Required] PlayRequest playRequest) + { + if (playRequest == null) + { + throw new ArgumentException("Request Body may not be null"); + } + + playRequest.ItemIds = itemIds; + playRequest.StartPositionTicks = startPositionTicks; + playRequest.PlayCommand = playCommand; + + _sessionManager.SendPlayCommand( + RequestHelpers.GetSession(_sessionContext).Id, + id, + playRequest, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a playstate command to a client. + /// + /// The session id. + /// The . + /// Playstate command sent to session. + /// A . + [HttpPost("/Sessions/{id}/Playing/{command}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendPlaystateCommand( + [FromRoute] string id, + [FromBody] PlaystateRequest playstateRequest) + { + _sessionManager.SendPlaystateCommand( + RequestHelpers.GetSession(_sessionContext).Id, + id, + playstateRequest, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a system command to a client. + /// + /// The session id. + /// The command to send. + /// System command sent to session. + /// A . + [HttpPost("/Sessions/{id}/System/{Command}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendSystemCommand( + [FromRoute] string id, + [FromRoute] string command) + { + var name = command; + if (Enum.TryParse(name, true, out GeneralCommandType commandType)) + { + name = commandType.ToString(); + } + + var currentSession = RequestHelpers.GetSession(_sessionContext); + var generalCommand = new GeneralCommand + { + Name = name, + ControllingUserId = currentSession.UserId + }; + + _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a general command to a client. + /// + /// The session id. + /// The command to send. + /// General command sent to session. + /// A . + [HttpPost("/Sessions/{id}/Command/{Command}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendGeneralCommand( + [FromRoute] string id, + [FromRoute] string command) + { + var currentSession = RequestHelpers.GetSession(_sessionContext); + + var generalCommand = new GeneralCommand + { + Name = command, + ControllingUserId = currentSession.UserId + }; + + _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a full general command to a client. + /// + /// The session id. + /// The . + /// Full general command sent to session. + /// A . + [HttpPost("/Sessions/{id}/Command")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendFullGeneralCommand( + [FromRoute] string id, + [FromBody, Required] GeneralCommand command) + { + var currentSession = RequestHelpers.GetSession(_sessionContext); + + if (command == null) + { + throw new ArgumentException("Request body may not be null"); + } + + command.ControllingUserId = currentSession.UserId; + + _sessionManager.SendGeneralCommand( + currentSession.Id, + id, + command, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a command to a client to display a message to the user. + /// + /// The session id. + /// The message test. + /// The message header. + /// The message timeout. If omitted the user will have to confirm viewing the message. + /// Message sent. + /// A . + [HttpPost("/Sessions/{id}/Message")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendMessageCommand( + [FromRoute] string id, + [FromQuery] string text, + [FromQuery] string header, + [FromQuery] long? timeoutMs) + { + var command = new MessageCommand + { + Header = string.IsNullOrEmpty(header) ? "Message from Server" : header, + TimeoutMs = timeoutMs, + Text = text + }; + + _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionContext).Id, id, command, CancellationToken.None); + + return NoContent(); + } + + /// + /// Adds an additional user to a session. + /// + /// The session id. + /// The user id. + /// User added to session. + /// A . + [HttpPost("/Sessions/{id}/User/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddUserToSession( + [FromRoute] string id, + [FromRoute] Guid userId) + { + _sessionManager.AddAdditionalUser(id, userId); + return NoContent(); + } + + /// + /// Removes an additional user from a session. + /// + /// The session id. + /// The user id. + /// User removed from session. + /// A . + [HttpDelete("/Sessions/{id}/User/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveUserFromSession( + [FromRoute] string id, + [FromRoute] Guid userId) + { + _sessionManager.RemoveAdditionalUser(id, userId); + return NoContent(); + } + + /// + /// Updates capabilities for a device. + /// + /// The session id. + /// A list of playable media types, comma delimited. Audio, Video, Book, Photo. + /// A list of supported remote control commands, comma delimited. + /// Determines whether media can be played remotely.. + /// Determines whether sync is supported. + /// Determines whether the device supports a unique identifier. + /// Capabilities posted. + /// A . + [HttpPost("/Sessions/Capabilities")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PostCapabilities( + [FromQuery] string id, + [FromQuery] string playableMediaTypes, + [FromQuery] string supportedCommands, + [FromQuery] bool supportsMediaControl, + [FromQuery] bool supportsSync, + [FromQuery] bool supportsPersistentIdentifier = true) + { + if (string.IsNullOrWhiteSpace(id)) + { + id = RequestHelpers.GetSession(_sessionContext).Id; + } + + _sessionManager.ReportCapabilities(id, new ClientCapabilities + { + PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), + SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true), + SupportsMediaControl = supportsMediaControl, + SupportsSync = supportsSync, + SupportsPersistentIdentifier = supportsPersistentIdentifier + }); + return NoContent(); + } + + /// + /// Updates capabilities for a device. + /// + /// The session id. + /// The . + /// Capabilities updated. + /// A . + [HttpPost("/Sessions/Capabilities/Full")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PostFullCapabilities( + [FromQuery] string id, + [FromBody, Required] ClientCapabilities capabilities) + { + if (string.IsNullOrWhiteSpace(id)) + { + id = RequestHelpers.GetSession(_sessionContext).Id; + } + + _sessionManager.ReportCapabilities(id, capabilities); + + return NoContent(); + } + + /// + /// Reports that a session is viewing an item. + /// + /// The session id. + /// The item id. + /// Session reported to server. + /// A . + [HttpPost("/Sessions/Viewing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult ReportViewing( + [FromQuery] string sessionId, + [FromQuery] string itemId) + { + string session = RequestHelpers.GetSession(_sessionContext).Id; + + _sessionManager.ReportNowViewingItem(session, itemId); + return NoContent(); + } + + /// + /// Reports that a session has ended. + /// + /// Session end reported to server. + /// A . + [HttpPost("/Sessions/Logout")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult ReportSessionEnded() + { + // TODO: how do we get AuthorizationInfo without an IRequest? + AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request); + + _sessionManager.Logout(auth.Token); + return NoContent(); + } + + /// + /// Get all auth providers. + /// + /// Auth providers retrieved. + /// An with the auth providers. + [HttpGet("/Auth/Providers")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAuthProviders() + { + return _userManager.GetAuthenticationProviders(); + } + + /// + /// Get all password reset providers. + /// + /// Password reset providers retrieved. + /// An with the password reset providers. + [HttpGet("/Auto/PasswordResetProviders")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetPasswordResetProviders() + { + return _userManager.GetPasswordResetProviders(); + } + } +} diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 9f4d34f9c6..ae8ab37e8e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,4 +1,6 @@ using System; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; namespace Jellyfin.Api.Helpers { @@ -25,5 +27,18 @@ namespace Jellyfin.Api.Helpers ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) : value.Split(separator); } + + internal static SessionInfo GetSession(ISessionContext sessionContext) + { + // TODO: how do we get a SessionInfo without IRequest? + SessionInfo session = sessionContext.GetSession("Request"); + + if (session == null) + { + throw new ArgumentException("Session not found."); + } + + return session; + } } } diff --git a/MediaBrowser.Api/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs deleted file mode 100644 index 020bb5042b..0000000000 --- a/MediaBrowser.Api/Sessions/SessionService.cs +++ /dev/null @@ -1,498 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Session; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Sessions -{ - /// - /// Class GetSessions. - /// - [Route("/Sessions", "GET", Summary = "Gets a list of sessions")] - [Authenticated] - public class GetSessions : IReturn - { - [ApiMember(Name = "ControllableByUserId", Description = "Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid ControllableByUserId { get; set; } - - [ApiMember(Name = "DeviceId", Description = "Filter by device Id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string DeviceId { get; set; } - - public int? ActiveWithinSeconds { get; set; } - } - - /// - /// Class DisplayContent. - /// - [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")] - [Authenticated] - public class DisplayContent : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Artist, Genre, Studio, Person, or any kind of BaseItem - /// - /// The type of the item. - [ApiMember(Name = "ItemType", Description = "The type of item to browse to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemType { get; set; } - - /// - /// Artist name, genre name, item Id, etc - /// - /// The item identifier. - [ApiMember(Name = "ItemId", Description = "The Id of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemId { get; set; } - - /// - /// Gets or sets the name of the item. - /// - /// The name of the item. - [ApiMember(Name = "ItemName", Description = "The name of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemName { get; set; } - } - - [Route("/Sessions/{Id}/Playing", "POST", Summary = "Instructs a session to play an item")] - [Authenticated] - public class Play : PlayRequest - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/{Id}/Playing/{Command}", "POST", Summary = "Issues a playstate command to a client")] - [Authenticated] - public class SendPlaystateCommand : PlaystateRequest, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/{Id}/System/{Command}", "POST", Summary = "Issues a system command to a client")] - [Authenticated] - public class SendSystemCommand : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the command. - /// - /// The play command. - [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Command { get; set; } - } - - [Route("/Sessions/{Id}/Command/{Command}", "POST", Summary = "Issues a system command to a client")] - [Authenticated] - public class SendGeneralCommand : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the command. - /// - /// The play command. - [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Command { get; set; } - } - - [Route("/Sessions/{Id}/Command", "POST", Summary = "Issues a system command to a client")] - [Authenticated] - public class SendFullGeneralCommand : GeneralCommand, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/{Id}/Message", "POST", Summary = "Issues a command to a client to display a message to the user")] - [Authenticated] - public class SendMessageCommand : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "Text", Description = "The message text.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Text { get; set; } - - [ApiMember(Name = "Header", Description = "The message header.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Header { get; set; } - - [ApiMember(Name = "TimeoutMs", Description = "The message timeout. If omitted the user will have to confirm viewing the message.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public long? TimeoutMs { get; set; } - } - - [Route("/Sessions/{Id}/Users/{UserId}", "POST", Summary = "Adds an additional user to a session")] - [Authenticated] - public class AddUserToSession : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "UserId", Description = "UserId Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } - } - - [Route("/Sessions/{Id}/Users/{UserId}", "DELETE", Summary = "Removes an additional user from a session")] - [Authenticated] - public class RemoveUserFromSession : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } - } - - [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")] - [Authenticated] - public class PostCapabilities : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string PlayableMediaTypes { get; set; } - - [ApiMember(Name = "SupportedCommands", Description = "A list of supported remote control commands, comma delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string SupportedCommands { get; set; } - - [ApiMember(Name = "SupportsMediaControl", Description = "Determines whether media can be played remotely.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool SupportsMediaControl { get; set; } - - [ApiMember(Name = "SupportsSync", Description = "Determines whether sync is supported.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool SupportsSync { get; set; } - - [ApiMember(Name = "SupportsPersistentIdentifier", Description = "Determines whether the device supports a unique identifier.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool SupportsPersistentIdentifier { get; set; } - - public PostCapabilities() - { - SupportsPersistentIdentifier = true; - } - } - - [Route("/Sessions/Capabilities/Full", "POST", Summary = "Updates capabilities for a device")] - [Authenticated] - public class PostFullCapabilities : ClientCapabilities, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/Viewing", "POST", Summary = "Reports that a session is viewing an item")] - [Authenticated] - public class ReportViewing : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "ItemId", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemId { get; set; } - } - - [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")] - [Authenticated] - public class ReportSessionEnded : IReturnVoid - { - } - - [Route("/Auth/Providers", "GET")] - [Authenticated(Roles = "Admin")] - public class GetAuthProviders : IReturn - { - } - - [Route("/Auth/PasswordResetProviders", "GET")] - [Authenticated(Roles = "Admin")] - public class GetPasswordResetProviders : IReturn - { - } - - /// - /// Class SessionsService. - /// - public class SessionService : BaseApiService - { - /// - /// The session manager. - /// - private readonly ISessionManager _sessionManager; - - private readonly IUserManager _userManager; - private readonly IAuthorizationContext _authContext; - private readonly IDeviceManager _deviceManager; - private readonly ISessionContext _sessionContext; - - public SessionService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISessionManager sessionManager, - IUserManager userManager, - IAuthorizationContext authContext, - IDeviceManager deviceManager, - ISessionContext sessionContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _sessionManager = sessionManager; - _userManager = userManager; - _authContext = authContext; - _deviceManager = deviceManager; - _sessionContext = sessionContext; - } - - public object Get(GetAuthProviders request) - { - return _userManager.GetAuthenticationProviders(); - } - - public object Get(GetPasswordResetProviders request) - { - return _userManager.GetPasswordResetProviders(); - } - - public void Post(ReportSessionEnded request) - { - var auth = _authContext.GetAuthorizationInfo(Request); - - _sessionManager.Logout(auth.Token); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetSessions request) - { - var result = _sessionManager.Sessions; - - if (!string.IsNullOrEmpty(request.DeviceId)) - { - result = result.Where(i => string.Equals(i.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase)); - } - - if (!request.ControllableByUserId.Equals(Guid.Empty)) - { - result = result.Where(i => i.SupportsRemoteControl); - - var user = _userManager.GetUserById(request.ControllableByUserId); - - if (!user.Policy.EnableRemoteControlOfOtherUsers) - { - result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId)); - } - - if (!user.Policy.EnableSharedDeviceControl) - { - result = result.Where(i => !i.UserId.Equals(Guid.Empty)); - } - - if (request.ActiveWithinSeconds.HasValue && request.ActiveWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - request.ActiveWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - - result = result.Where(i => - { - var deviceId = i.DeviceId; - - if (!string.IsNullOrWhiteSpace(deviceId)) - { - if (!_deviceManager.CanAccessDevice(user, deviceId)) - { - return false; - } - } - - return true; - }); - } - - return ToOptimizedResult(result.ToArray()); - } - - public Task Post(SendPlaystateCommand request) - { - return _sessionManager.SendPlaystateCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(DisplayContent request) - { - var command = new BrowseRequest - { - ItemId = request.ItemId, - ItemName = request.ItemName, - ItemType = request.ItemType - }; - - return _sessionManager.SendBrowseCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(SendSystemCommand request) - { - var name = request.Command; - if (Enum.TryParse(name, true, out GeneralCommandType commandType)) - { - name = commandType.ToString(); - } - - var currentSession = GetSession(_sessionContext); - var command = new GeneralCommand - { - Name = name, - ControllingUserId = currentSession.UserId - }; - - return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(SendMessageCommand request) - { - var command = new MessageCommand - { - Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header, - TimeoutMs = request.TimeoutMs, - Text = request.Text - }; - - return _sessionManager.SendMessageCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(Play request) - { - return _sessionManager.SendPlayCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None); - } - - public Task Post(SendGeneralCommand request) - { - var currentSession = GetSession(_sessionContext); - - var command = new GeneralCommand - { - Name = request.Command, - ControllingUserId = currentSession.UserId - }; - - return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None); - } - - public Task Post(SendFullGeneralCommand request) - { - var currentSession = GetSession(_sessionContext); - - request.ControllingUserId = currentSession.UserId; - - return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, request, CancellationToken.None); - } - - public void Post(AddUserToSession request) - { - _sessionManager.AddAdditionalUser(request.Id, new Guid(request.UserId)); - } - - public void Delete(RemoveUserFromSession request) - { - _sessionManager.RemoveAdditionalUser(request.Id, new Guid(request.UserId)); - } - - public void Post(PostCapabilities request) - { - if (string.IsNullOrWhiteSpace(request.Id)) - { - request.Id = GetSession(_sessionContext).Id; - } - - _sessionManager.ReportCapabilities(request.Id, new ClientCapabilities - { - PlayableMediaTypes = SplitValue(request.PlayableMediaTypes, ','), - SupportedCommands = SplitValue(request.SupportedCommands, ','), - SupportsMediaControl = request.SupportsMediaControl, - SupportsSync = request.SupportsSync, - SupportsPersistentIdentifier = request.SupportsPersistentIdentifier - }); - } - - public void Post(PostFullCapabilities request) - { - if (string.IsNullOrWhiteSpace(request.Id)) - { - request.Id = GetSession(_sessionContext).Id; - } - - _sessionManager.ReportCapabilities(request.Id, request); - } - - public void Post(ReportViewing request) - { - request.SessionId = GetSession(_sessionContext).Id; - - _sessionManager.ReportNowViewingItem(request.SessionId, request.ItemId); - } - } -} From 720fff30a4da7490ce2ce6053cb496dbf19d6c8f Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 12 Jun 2020 14:37:55 -0600 Subject: [PATCH 182/724] readd swagger version --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 0097462430..c3c8716c0b 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -119,7 +119,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" }); c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, From 6d5c09c4990ecbc071afbe6611ecef75e5fe8b65 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 12 Jun 2020 14:40:06 -0600 Subject: [PATCH 183/724] Remove duplicate swaggerdoc --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index c3c8716c0b..86d547af04 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -139,7 +139,6 @@ namespace Jellyfin.Server.Extensions { { securitySchemeRef, Array.Empty() } }); - c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( From ec3e15db5789b6218482beb488433f41f9a0d8ba Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 13 Jun 2020 13:11:41 -0600 Subject: [PATCH 184/724] Fix merge and build --- Jellyfin.Api/BaseJellyfinApiController.cs | 4 -- .../Controllers/ActivityLogController.cs | 11 +++- .../ConfigurationDtos/MediaEncoderPathDto.cs | 6 +- .../ApiServiceCollectionExtensions.cs | 2 +- .../CamelCaseJsonProfileFormatter.cs | 2 +- .../PascalCaseJsonProfileFormatter.cs | 2 +- MediaBrowser.Api/MediaBrowser.Api.csproj | 4 -- MediaBrowser.Api/System/ActivityLogService.cs | 66 ------------------- .../JsonNonStringKeyDictionaryConverter.cs | 26 ++++---- ...nNonStringKeyDictionaryConverterFactory.cs | 5 +- MediaBrowser.Common/Json/JsonDefaults.cs | 30 ++++----- 11 files changed, 46 insertions(+), 112 deletions(-) delete mode 100644 MediaBrowser.Api/System/ActivityLogService.cs diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 615f330a4c..a34f9eb62f 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,8 +1,4 @@ -<<<<<<< HEAD -using System; -======= using System.Net.Mime; ->>>>>>> origin/master using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index 8d37a83738..895d9f719d 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,6 +1,10 @@ +#nullable enable +#pragma warning disable CA1801 + using System; -using System.Globalization; +using System.Linq; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; @@ -44,7 +48,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] DateTime? minDate, bool? hasUserId) { - return _activityManager.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); + var filterFunc = new Func, IQueryable>( + entries => entries.Where(entry => entry.DateCreated >= minDate)); + + return _activityManager.GetPagedResult(filterFunc, startIndex, limit); } } } diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs index b05e0cdf5a..3706a11e3a 100644 --- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs +++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Jellyfin.Api.Models.ConfigurationDtos { /// @@ -8,11 +10,11 @@ namespace Jellyfin.Api.Models.ConfigurationDtos /// /// Gets or sets media encoder path. /// - public string Path { get; set; } + public string Path { get; set; } = null!; /// /// Gets or sets media encoder path type. /// - public string PathType { get; set; } + public string PathType { get; set; } = null!; } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 86d547af04..9cdaa0eb16 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -93,7 +93,7 @@ namespace Jellyfin.Server.Extensions .AddJsonOptions(options => { // Update all properties that are set in JsonDefaults - var jsonOptions = JsonDefaults.PascalCase; + var jsonOptions = JsonDefaults.GetPascalCaseOptions(); // From JsonDefaults options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index 989c8ecea2..9b347ae2c2 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase) + public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions()) { 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 69963b3fb3..0024708bad 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase) + public PascalCaseJsonProfileFormatter() : base(JsonDefaults.GetPascalCaseOptions()) { SupportedMediaTypes.Clear(); // Add application/json for default formatter diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 0b0c5cc9fc..d703bdb058 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -14,10 +14,6 @@ - - - - netstandard2.1 false diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs deleted file mode 100644 index a6bacad4fc..0000000000 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using Jellyfin.Data.Entities; -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 filterFunc = new Func, IQueryable>( - entries => entries.Where(entry => entry.DateCreated >= minDate)); - - var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit); - - return ToOptimizedResult(result); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs index 636ef5372f..8053461f08 100644 --- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Json.Converters /// The type to convert. /// The json serializer options. /// Typed dictionary. - /// + /// Dictionary key type not supported. public override IDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]); @@ -38,24 +38,24 @@ namespace MediaBrowser.Common.Json.Converters 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) }, + "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); + instance.Add((TKey)parse.Invoke(null, new[] { (object?)element.Key }), element.Value); } - + return instance; } @@ -70,8 +70,12 @@ namespace MediaBrowser.Common.Json.Converters var convertedDictionary = new Dictionary(value.Count); foreach (var (k, v) in value) { - convertedDictionary[k?.ToString()] = v; + if (k != null) + { + convertedDictionary[k.ToString()] = v; + } } + JsonSerializer.Serialize(writer, convertedDictionary, options); } } diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs index d9795a189a..52f3607401 100644 --- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs @@ -22,18 +22,17 @@ namespace MediaBrowser.Common.Json.Converters /// 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; } diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index f38e2893ec..adc15123b1 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Common.Json /// When changing these options, update /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs /// -> AddJellyfinApi - /// -> AddJsonOptions + /// -> AddJsonOptions. /// /// The default options. public static JsonSerializerOptions GetOptions() @@ -33,31 +33,27 @@ namespace MediaBrowser.Common.Json return options; } - + /// - /// Gets CamelCase json options. + /// Gets camelCase json options. /// - public static JsonSerializerOptions CamelCase + /// The camelCase options. + public static JsonSerializerOptions GetCamelCaseOptions() { - get - { - var options = GetOptions(); - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - return options; - } + var options = GetOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + return options; } /// /// Gets PascalCase json options. /// - public static JsonSerializerOptions PascalCase + /// The PascalCase options. + public static JsonSerializerOptions GetPascalCaseOptions() { - get - { - var options = GetOptions(); - options.PropertyNamingPolicy = null; - return options; - } + var options = GetOptions(); + options.PropertyNamingPolicy = null; + return options; } } } From 552a74eb6e874976116447754785b6c1ca355718 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 13 Jun 2020 15:13:57 -0600 Subject: [PATCH 185/724] Fix build --- Jellyfin.Api/Controllers/Images/RemoteImageController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs index 1155cc653e..f521dfdf28 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs @@ -85,9 +85,8 @@ namespace Jellyfin.Api.Controllers.Images var images = await _providerManager.GetAvailableRemoteImages( item, - new RemoteImageQuery + new RemoteImageQuery(providerName) { - ProviderName = providerName, IncludeAllLanguages = includeAllLanguages, IncludeDisabledProviders = true, ImageType = type From 3c18745f5392d45c1f008f15438e91831fb39294 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 13 Jun 2020 15:15:27 -0600 Subject: [PATCH 186/724] Remove RemoteImageService.cs --- MediaBrowser.Api/Images/RemoteImageService.cs | 297 ------------------ 1 file changed, 297 deletions(-) delete mode 100644 MediaBrowser.Api/Images/RemoteImageService.cs diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs deleted file mode 100644 index 358ac30fae..0000000000 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Images -{ - public class BaseRemoteImageRequest : IReturn - { - [ApiMember(Name = "Type", Description = "The image type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public ImageType? Type { get; set; } - - /// - /// 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 = "ProviderName", Description = "Optional. The image provider to use", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ProviderName { get; set; } - - [ApiMember(Name = "IncludeAllLanguages", Description = "Optional.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool IncludeAllLanguages { get; set; } - } - - [Route("/Items/{Id}/RemoteImages", "GET", Summary = "Gets available remote images for an item")] - [Authenticated] - public class GetRemoteImages : BaseRemoteImageRequest - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Items/{Id}/RemoteImages/Providers", "GET", Summary = "Gets available remote image providers for an item")] - [Authenticated] - public class GetRemoteImageProviders : IReturn> - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - public class BaseDownloadRemoteImage : IReturnVoid - { - [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET,POST")] - public ImageType Type { get; set; } - - [ApiMember(Name = "ProviderName", Description = "The image provider", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] - public string ProviderName { get; set; } - - [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] - public string ImageUrl { get; set; } - } - - [Route("/Items/{Id}/RemoteImages/Download", "POST", Summary = "Downloads a remote image for an item")] - [Authenticated(Roles = "Admin")] - public class DownloadRemoteImage : BaseDownloadRemoteImage - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Images/Remote", "GET", Summary = "Gets a remote image")] - public class GetRemoteImage - { - [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ImageUrl { get; set; } - } - - public class RemoteImageService : BaseApiService - { - private readonly IProviderManager _providerManager; - - private readonly IServerApplicationPaths _appPaths; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - - private readonly ILibraryManager _libraryManager; - - public RemoteImageService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IProviderManager providerManager, - IServerApplicationPaths appPaths, - IHttpClient httpClient, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _providerManager = providerManager; - _appPaths = appPaths; - _httpClient = httpClient; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - } - - public object Get(GetRemoteImageProviders request) - { - var item = _libraryManager.GetItemById(request.Id); - - var result = GetImageProviders(item); - - return ToOptimizedResult(result); - } - - private List GetImageProviders(BaseItem item) - { - return _providerManager.GetRemoteImageProviderInfo(item).ToList(); - } - - public async Task Get(GetRemoteImages request) - { - var item = _libraryManager.GetItemById(request.Id); - - var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery(request.ProviderName) - { - IncludeAllLanguages = request.IncludeAllLanguages, - IncludeDisabledProviders = true, - ImageType = request.Type - - }, CancellationToken.None).ConfigureAwait(false); - - var imagesList = images.ToArray(); - - var allProviders = _providerManager.GetRemoteImageProviderInfo(item); - - if (request.Type.HasValue) - { - allProviders = allProviders.Where(i => i.SupportedImages.Contains(request.Type.Value)); - } - - var result = new RemoteImageResult - { - TotalRecordCount = imagesList.Length, - Providers = allProviders.Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() - }; - - if (request.StartIndex.HasValue) - { - imagesList = imagesList.Skip(request.StartIndex.Value) - .ToArray(); - } - - if (request.Limit.HasValue) - { - imagesList = imagesList.Take(request.Limit.Value) - .ToArray(); - } - - result.Images = imagesList; - - return result; - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(DownloadRemoteImage request) - { - var item = _libraryManager.GetItemById(request.Id); - - return DownloadRemoteImage(item, request); - } - - /// - /// Downloads the remote image. - /// - /// The item. - /// The request. - /// Task. - private async Task DownloadRemoteImage(BaseItem item, BaseDownloadRemoteImage request) - { - await _providerManager.SaveImage(item, request.ImageUrl, request.Type, null, CancellationToken.None).ConfigureAwait(false); - - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task Get(GetRemoteImage request) - { - var urlHash = request.ImageUrl.GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string contentPath; - - try - { - contentPath = File.ReadAllText(pointerCachePath); - - if (File.Exists(contentPath)) - { - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - } - catch (FileNotFoundException) - { - // Means the file isn't cached yet - } - catch (IOException) - { - // Means the file isn't cached yet - } - - await DownloadImage(request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - - // Read the pointer file again - contentPath = File.ReadAllText(pointerCachePath); - - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - - /// - /// Downloads the image. - /// - /// The URL. - /// The URL hash. - /// The pointer cache path. - /// Task. - private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) - { - using var result = await _httpClient.GetResponse(new HttpRequestOptions - { - Url = url, - BufferContent = false - }).ConfigureAwait(false); - var ext = result.ContentType.Split('/')[^1]; - - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - var stream = result.Content; - await using (stream.ConfigureAwait(false)) - { - var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); - await using (filestream.ConfigureAwait(false)) - { - await stream.CopyToAsync(filestream).ConfigureAwait(false); - } - } - - Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); - File.WriteAllText(pointerCachePath, fullCachePath); - } - - /// - /// Gets the full cache path. - /// - /// The filename. - /// System.String. - private string GetFullCachePath(string filename) - { - return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); - } - } -} From 2d4998c5782d426f09aa264f37a2bc384bf940f2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 13 Jun 2020 15:28:04 -0600 Subject: [PATCH 187/724] fix build --- Jellyfin.Api/Controllers/EnvironmentController.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 35cd89e0e8..046ffdf8eb 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -64,12 +64,7 @@ namespace Jellyfin.Api.Controllers .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles)) .OrderBy(i => i.FullName); - return entries.Select(f => new FileSystemEntryInfo - { - Name = f.Name, - Path = f.FullName, - Type = f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File - }); + return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File)); } /// @@ -151,12 +146,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetDrives() { - return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo - { - Name = d.Name, - Path = d.FullName, - Type = FileSystemEntryType.Directory - }); + return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory)); } /// From 13b53db4efd14926a519b9cd6006c51a3536565a Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 13 Jun 2020 15:31:22 -0600 Subject: [PATCH 188/724] fix build --- Jellyfin.Api/Helpers/RequestHelpers.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 1e9aa7b43c..6b2d014a1d 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Helpers /// Sort By. Comma delimited string. /// Sort Order. Comma delimited string. /// Order By. - public static ValueTuple[] GetOrderBy(string sortBy, string requestedSortOrder) + public static ValueTuple[] GetOrderBy(string? sortBy, string? requestedSortOrder) { var val = sortBy; @@ -56,7 +56,7 @@ namespace Jellyfin.Api.Helpers /// /// The fields. /// IEnumerable{ItemFields}. - public static ItemFields[] GetItemFields(string fields) + public static ItemFields[] GetItemFields(string? fields) { if (string.IsNullOrEmpty(fields)) { @@ -79,7 +79,7 @@ namespace Jellyfin.Api.Helpers /// /// The filters. /// Item filters. - public static IEnumerable GetFilters(string filters) + public static IEnumerable GetFilters(string? filters) { return string.IsNullOrEmpty(filters) ? Array.Empty() From 276750f310b741b1610f9abaff66c204571c58ed Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 13 Jun 2020 15:33:42 -0600 Subject: [PATCH 189/724] Move ItemRefreshService.cs to Jellyfin.Api --- .../Controllers/ItemRefreshController.cs | 89 +++++++++++++++++++ MediaBrowser.Api/ItemRefreshService.cs | 83 ----------------- 2 files changed, 89 insertions(+), 83 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ItemRefreshController.cs delete mode 100644 MediaBrowser.Api/ItemRefreshService.cs diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs new file mode 100644 index 0000000000..d9b8357d2e --- /dev/null +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -0,0 +1,89 @@ +#nullable enable +#pragma warning disable CA1801 + +using System.ComponentModel; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Item Refresh Controller. + /// + /// [Authenticated] + [Route("/Items")] + [Authorize] + public class ItemRefreshController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public ItemRefreshController( + ILibraryManager libraryManager, + IProviderManager providerManager, + IFileSystem fileSystem) + { + _libraryManager = libraryManager; + _providerManager = providerManager; + _fileSystem = fileSystem; + } + + /// + /// Refreshes metadata for an item. + /// + /// Item id. + /// (Optional) Specifies the metadata refresh mode. + /// (Optional) Specifies the image refresh mode. + /// (Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh. + /// (Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh. + /// (Unused) Indicates if the refresh should occur recursively. + /// Item metadata refresh queued. + /// Item to refresh not found. + /// An on success, or a if the item could not be found. + [HttpPost("{Id}/Refresh")] + [Description("Refreshes metadata for an item.")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult Post( + [FromRoute] string id, + [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, + [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None, + [FromQuery] bool replaceAllMetadata = false, + [FromQuery] bool replaceAllImages = false, + [FromQuery] bool recursive = false) + { + var item = _libraryManager.GetItemById(id); + if (item == null) + { + return NotFound(); + } + + var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + MetadataRefreshMode = metadataRefreshMode, + ImageRefreshMode = imageRefreshMode, + ReplaceAllImages = replaceAllImages, + ReplaceAllMetadata = replaceAllMetadata, + ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh + || imageRefreshMode == MetadataRefreshMode.FullRefresh + || replaceAllImages + || replaceAllMetadata, + IsAutomated = false + }; + + _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High); + return Ok(); + } + } +} diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs deleted file mode 100644 index 5e86f04a82..0000000000 --- a/MediaBrowser.Api/ItemRefreshService.cs +++ /dev/null @@ -1,83 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - public class BaseRefreshRequest : IReturnVoid - { - [ApiMember(Name = "MetadataRefreshMode", Description = "Specifies the metadata refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] - public MetadataRefreshMode MetadataRefreshMode { get; set; } - - [ApiMember(Name = "ImageRefreshMode", Description = "Specifies the image refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] - public MetadataRefreshMode ImageRefreshMode { get; set; } - - [ApiMember(Name = "ReplaceAllMetadata", Description = "Determines if metadata should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] - public bool ReplaceAllMetadata { get; set; } - - [ApiMember(Name = "ReplaceAllImages", Description = "Determines if images should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] - public bool ReplaceAllImages { get; set; } - } - - [Route("/Items/{Id}/Refresh", "POST", Summary = "Refreshes metadata for an item")] - public class RefreshItem : BaseRefreshRequest - { - [ApiMember(Name = "Recursive", Description = "Indicates if the refresh should occur recursively.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool Recursive { get; set; } - - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Authenticated] - public class ItemRefreshService : BaseApiService - { - private readonly ILibraryManager _libraryManager; - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - - public ItemRefreshService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - IProviderManager providerManager, - IFileSystem fileSystem) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _libraryManager = libraryManager; - _providerManager = providerManager; - _fileSystem = fileSystem; - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(RefreshItem request) - { - var item = _libraryManager.GetItemById(request.Id); - - var options = GetRefreshOptions(request); - - _providerManager.QueueRefresh(item.Id, options, RefreshPriority.High); - } - - private MetadataRefreshOptions GetRefreshOptions(RefreshItem request) - { - return new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - MetadataRefreshMode = request.MetadataRefreshMode, - ImageRefreshMode = request.ImageRefreshMode, - ReplaceAllImages = request.ReplaceAllImages, - ReplaceAllMetadata = request.ReplaceAllMetadata, - ForceSave = request.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || request.ImageRefreshMode == MetadataRefreshMode.FullRefresh || request.ReplaceAllImages || request.ReplaceAllMetadata, - IsAutomated = false - }; - } - } -} From edc5611ec7c638164ffe14e4c06055d4fd58b5e8 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 14 Jun 2020 13:50:51 +0200 Subject: [PATCH 190/724] Fix null reference to fix CI --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index f074a61dbe..ecbfed4693 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,4 +1,4 @@ -#pragma warning disable CA1801 +#pragma warning disable CA1801 using System; using System.Collections.Generic; @@ -43,7 +43,7 @@ namespace Jellyfin.Api.Controllers ILibraryManager libraryManager, ILibraryMonitor libraryMonitor) { - _appPaths = serverConfigurationManager?.ApplicationPaths; + _appPaths = serverConfigurationManager.ApplicationPaths; _libraryManager = libraryManager; _libraryMonitor = libraryMonitor; } From 524243a9340bfccd2c9ae708be70a5e49f5f53f1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 14 Jun 2020 20:18:06 -0600 Subject: [PATCH 191/724] fix merge conflict --- Jellyfin.Api/Controllers/NotificationsController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 5af1947562..a76675d5a9 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using Jellyfin.Api.Models.NotificationDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Dto; @@ -115,7 +116,10 @@ namespace Jellyfin.Api.Controllers Description = description, Url = url, Level = level ?? NotificationLevel.Normal, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), + UserIds = _userManager.Users + .Where(user => user.HasPermission(PermissionKind.IsAdministrator)) + .Select(user => user.Id) + .ToArray(), Date = DateTime.UtcNow, }; From 4aac93672115d96ab77534f2b6a32a23482dab38 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 15 Jun 2020 12:49:54 -0600 Subject: [PATCH 192/724] Add more authorization handlers, actually authorize requests --- .../HttpServer/Security/AuthService.cs | 45 ++++---- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 102 ++++++++++++++++++ .../Auth/CustomAuthenticationHandler.cs | 25 +++-- .../DefaultAuthorizationHandler.cs | 42 ++++++++ .../DefaultAuthorizationRequirement.cs | 11 ++ .../FirstTimeSetupOrElevatedHandler.cs | 21 +++- .../IgnoreScheduleHandler.cs | 42 ++++++++ .../IgnoreScheduleRequirement.cs | 11 ++ .../LocalAccessPolicy/LocalAccessHandler.cs | 44 ++++++++ .../LocalAccessRequirement.cs | 11 ++ .../RequiresElevationHandler.cs | 26 ++++- Jellyfin.Api/Constants/InternalClaimTypes.cs | 38 +++++++ Jellyfin.Api/Constants/Policies.cs | 15 +++ Jellyfin.Api/Helpers/ClaimHelpers.cs | 77 +++++++++++++ .../ApiServiceCollectionExtensions.cs | 31 +++++- MediaBrowser.Controller/Net/IAuthService.cs | 25 ++++- 16 files changed, 525 insertions(+), 41 deletions(-) create mode 100644 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs create mode 100644 Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs create mode 100644 Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs create mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs create mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs create mode 100644 Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs create mode 100644 Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs create mode 100644 Jellyfin.Api/Constants/InternalClaimTypes.cs create mode 100644 Jellyfin.Api/Helpers/ClaimHelpers.cs diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index c65d4694aa..6a2d8fdbbb 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security _networkManager = networkManager; } - public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues) + public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes) { - ValidateUser(request, authAttribtues); + ValidateUser(request, authAttributes); } public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) @@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security return user; } - private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues) + public AuthorizationInfo Authenticate(HttpRequest request) + { + var auth = _authorizationContext.GetAuthorizationInfo(request); + if (auth?.User == null) + { + return null; + } + + if (auth.User.HasPermission(PermissionKind.IsDisabled)) + { + throw new SecurityException("User account has been disabled."); + } + + return auth; + } + + private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes) { // This code is executed before the service var auth = _authorizationContext.GetAuthorizationInfo(request); - if (!IsExemptFromAuthenticationToken(authAttribtues, request)) + if (!IsExemptFromAuthenticationToken(authAttributes, request)) { ValidateSecurityToken(request, auth.Token); } - if (authAttribtues.AllowLocalOnly && !request.IsLocal) + if (authAttributes.AllowLocalOnly && !request.IsLocal) { throw new SecurityException("Operation not found."); } @@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security if (user != null) { - ValidateUserAccess(user, request, authAttribtues, auth); + ValidateUserAccess(user, request, authAttributes); } var info = GetTokenInfo(request); - if (!IsExemptFromRoles(auth, authAttribtues, request, info)) + if (!IsExemptFromRoles(auth, authAttributes, request, info)) { - var roles = authAttribtues.GetRoles(); + var roles = authAttributes.GetRoles(); ValidateRoles(roles, user); } @@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security private void ValidateUserAccess( User user, IRequest request, - IAuthenticationAttributes authAttributes, - AuthorizationInfo auth) + IAuthenticationAttributes authAttributes) { if (user.HasPermission(PermissionKind.IsDisabled)) { @@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security { throw new AuthenticationException("Access token is invalid or expired."); } - - //if (!string.IsNullOrEmpty(info.UserId)) - //{ - // var user = _userManager.GetUserById(info.UserId); - - // if (user == null || user.Configuration.IsDisabled) - // { - // throw new SecurityException("User account has been disabled."); - // } - //} } } } diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs new file mode 100644 index 0000000000..b5b9d89041 --- /dev/null +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -0,0 +1,102 @@ +#nullable enable + +using System.Net; +using System.Security.Claims; +using Jellyfin.Api.Helpers; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth +{ + /// + /// Base authorization handler. + /// + /// Type of Authorization Requirement. + public abstract class BaseAuthorizationHandler : AuthorizationHandler + where T : IAuthorizationRequirement + { + private readonly IUserManager _userManager; + private readonly INetworkManager _networkManager; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + protected BaseAuthorizationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + { + _userManager = userManager; + _networkManager = networkManager; + _httpContextAccessor = httpContextAccessor; + } + + /// + /// Validate authenticated claims. + /// + /// Request claims. + /// Whether to ignore parental control. + /// Whether access is to be allowed locally only. + /// Validated claim status. + protected bool ValidateClaims( + ClaimsPrincipal claimsPrincipal, + bool ignoreSchedule = false, + bool localAccessOnly = false) + { + // Ensure claim has userId. + var userId = ClaimHelpers.GetUserId(claimsPrincipal); + if (userId == null) + { + return false; + } + + // Ensure userId links to a valid user. + var user = _userManager.GetUserById(userId.Value); + if (user == null) + { + return false; + } + + // Ensure user is not disabled. + if (user.HasPermission(PermissionKind.IsDisabled)) + { + return false; + } + + var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString(); + var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip); + // User cannot access remotely and user is remote + if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork) + { + return false; + } + + if (localAccessOnly && !isInLocalNetwork) + { + return false; + } + + // User attempting to access out of parental control hours. + if (!ignoreSchedule + && !user.HasPermission(PermissionKind.IsAdministrator) + && !user.IsParentalScheduleAllowed()) + { + return false; + } + + return true; + } + + private static IPAddress NormalizeIp(IPAddress ip) + { + return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip; + } + } +} diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index a5c4e9974a..d4d40da577 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -1,3 +1,6 @@ +#nullable enable + +using System.Globalization; using System.Security.Authentication; using System.Security.Claims; using System.Text.Encodings.Web; @@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth /// protected override Task HandleAuthenticateAsync() { - var authenticatedAttribute = new AuthenticatedAttribute - { - IgnoreLegacyAuth = true - }; - try { - var user = _authService.Authenticate(Request, authenticatedAttribute); - if (user == null) + var authorizationInfo = _authService.Authenticate(Request); + if (authorizationInfo == null) { return Task.FromResult(AuthenticateResult.NoResult()); // TODO return when legacy API is removed. @@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth var claims = new[] { - new Claim(ClaimTypes.Name, user.Username), - new Claim( - ClaimTypes.Role, - value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User) + new Claim(ClaimTypes.Name, authorizationInfo.User.Username), + new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User), + new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId), + new Claim(InternalClaimTypes.Device, authorizationInfo.Device), + new Claim(InternalClaimTypes.Client, authorizationInfo.Client), + new Claim(InternalClaimTypes.Version, authorizationInfo.Version), + new Claim(InternalClaimTypes.Token, authorizationInfo.Token) }; + var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs new file mode 100644 index 0000000000..b5913daab9 --- /dev/null +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy +{ + /// + /// Default authorization handler. + /// + public class DefaultAuthorizationHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public DefaultAuthorizationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) + { + var validated = ValidateClaims(context.User); + if (!validated) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs new file mode 100644 index 0000000000..7cea00b694 --- /dev/null +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy +{ + /// + /// The default authorization requirement. + /// + public class DefaultAuthorizationRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index 34aa5d12c8..0b12f7d3c2 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,22 +1,33 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy { /// /// Authorization handler for requiring first time setup or elevated privileges. /// - public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler + public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler { private readonly IConfigurationManager _configurationManager; /// /// Initializes a new instance of the class. /// - /// The jellyfin configuration manager. - public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager) + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public FirstTimeSetupOrElevatedHandler( + IConfigurationManager configurationManager, + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) { _configurationManager = configurationManager; } @@ -28,7 +39,9 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy { context.Succeed(firstTimeSetupOrElevatedRequirement); } - else if (context.User.IsInRole(UserRoles.Administrator)) + + var validated = ValidateClaims(context.User); + if (validated && context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(firstTimeSetupOrElevatedRequirement); } diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs new file mode 100644 index 0000000000..9afa0b28f1 --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy +{ + /// + /// Escape schedule controls handler. + /// + public class IgnoreScheduleHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public IgnoreScheduleHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement) + { + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (!validated) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs new file mode 100644 index 0000000000..d5bb61ce6c --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy +{ + /// + /// Escape schedule controls requirement. + /// + public class IgnoreScheduleRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs new file mode 100644 index 0000000000..af73352bcc --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.LocalAccessPolicy +{ + /// + /// Local access handler. + /// + public class LocalAccessHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public LocalAccessHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement) + { + var validated = ValidateClaims(context.User, localAccessOnly: true); + if (!validated) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs new file mode 100644 index 0000000000..761127fa40 --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.LocalAccessPolicy +{ + /// + /// The local access authorization requirement. + /// + public class LocalAccessRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index 2d3bb1aa48..b235c4b63b 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,21 +1,43 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.RequiresElevationPolicy { /// /// Authorization handler for requiring elevated privileges. /// - public class RequiresElevationHandler : AuthorizationHandler + public class RequiresElevationHandler : BaseAuthorizationHandler { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public RequiresElevationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement) { - if (context.User.IsInRole(UserRoles.Administrator)) + var validated = ValidateClaims(context.User); + if (validated && context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); } + else + { + context.Fail(); + } return Task.CompletedTask; } diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs new file mode 100644 index 0000000000..4d7c7135d5 --- /dev/null +++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs @@ -0,0 +1,38 @@ +namespace Jellyfin.Api.Constants +{ + /// + /// Internal claim types for authorization. + /// + public static class InternalClaimTypes + { + /// + /// User Id. + /// + public const string UserId = "Jellyfin-UserId"; + + /// + /// Device Id. + /// + public const string DeviceId = "Jellyfin-DeviceId"; + + /// + /// Device. + /// + public const string Device = "Jellyfin-Device"; + + /// + /// Client. + /// + public const string Client = "Jellyfin-Client"; + + /// + /// Version. + /// + public const string Version = "Jellyfin-Version"; + + /// + /// Token. + /// + public const string Token = "Jellyfin-Token"; + } +} diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index e2b383f75d..cf574e43df 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -5,6 +5,11 @@ namespace Jellyfin.Api.Constants /// public static class Policies { + /// + /// Policy name for default authorization. + /// + public const string DefaultAuthorization = "DefaultAuthorization"; + /// /// Policy name for requiring first time setup or elevated privileges. /// @@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants /// Policy name for requiring elevated privileges. /// public const string RequiresElevation = "RequiresElevation"; + + /// + /// Policy name for allowing local access only. + /// + public const string LocalAccessOnly = "LocalAccessOnly"; + + /// + /// Policy name for escaping schedule controls. + /// + public const string IgnoreSchedule = "IgnoreSchedule"; } } diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs new file mode 100644 index 0000000000..a07d4ed820 --- /dev/null +++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs @@ -0,0 +1,77 @@ +#nullable enable + +using System; +using System.Linq; +using System.Security.Claims; +using Jellyfin.Api.Constants; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Claim Helpers. + /// + public static class ClaimHelpers + { + /// + /// Get user id from claims. + /// + /// Current claims principal. + /// User id. + public static Guid? GetUserId(in ClaimsPrincipal user) + { + var value = GetClaimValue(user, InternalClaimTypes.UserId); + return string.IsNullOrEmpty(value) + ? null + : (Guid?)Guid.Parse(value); + } + + /// + /// Get device id from claims. + /// + /// Current claims principal. + /// Device id. + public static string? GetDeviceId(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.DeviceId); + + /// + /// Get device from claims. + /// + /// Current claims principal. + /// Device. + public static string? GetDevice(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Device); + + /// + /// Get client from claims. + /// + /// Current claims principal. + /// Client. + public static string? GetClient(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Client); + + /// + /// Get version from claims. + /// + /// Current claims principal. + /// Version. + public static string? GetVersion(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Version); + + /// + /// Get token from claims. + /// + /// Current claims principal. + /// Token. + public static string? GetToken(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Token); + + private static string? GetClaimValue(in ClaimsPrincipal user, string name) + { + return user?.Identities + .SelectMany(c => c.Claims) + .Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase)) + .Select(claim => claim.Value) + .FirstOrDefault(); + } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 9cdaa0eb16..1ec77d716c 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -5,7 +5,10 @@ using System.Linq; using System.Reflection; using Jellyfin.Api; using Jellyfin.Api.Auth; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; +using Jellyfin.Api.Auth.IgnoreSchedulePolicy; +using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; @@ -33,16 +36,19 @@ namespace Jellyfin.Server.Extensions /// The updated service collection. public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection) { + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); return serviceCollection.AddAuthorizationCore(options => { options.AddPolicy( - Policies.RequiresElevation, + Policies.DefaultAuthorization, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new RequiresElevationRequirement()); + policy.AddRequirements(new DefaultAuthorizationRequirement()); }); options.AddPolicy( Policies.FirstTimeSetupOrElevated, @@ -51,6 +57,27 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement()); }); + options.AddPolicy( + Policies.IgnoreSchedule, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new IgnoreScheduleRequirement()); + }); + options.AddPolicy( + Policies.LocalAccessOnly, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new LocalAccessRequirement()); + }); + options.AddPolicy( + Policies.RequiresElevation, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new RequiresElevationRequirement()); + }); }); } diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index d8f6d19da0..2055a656a7 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -6,10 +6,31 @@ using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { + /// + /// IAuthService. + /// public interface IAuthService { - void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues); + /// + /// Authenticate and authorize request. + /// + /// Request. + /// Authorization attributes. + void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes); - User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues); + /// + /// Authenticate and authorize request. + /// + /// Request. + /// Authorization attributes. + /// Authenticated user. + User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes); + + /// + /// Authenticate request. + /// + /// The request. + /// Authorization information. Null if unauthenticated. + AuthorizationInfo Authenticate(HttpRequest request); } } From a8adbef74fc8300190c463a9c585b55dcfb0c78e Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 15 Jun 2020 13:21:18 -0600 Subject: [PATCH 193/724] Add GetAuthorizationInfo for netcore HttpRequest --- .../Security/AuthorizationContext.cs | 138 ++++++++++++------ .../Net/IAuthorizationContext.cs | 11 ++ 2 files changed, 107 insertions(+), 42 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index 9558cb4c66..deb9b5ebb0 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer.Security @@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security return GetAuthorization(requestContext); } + public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext) + { + var auth = GetAuthorizationDictionary(requestContext); + var (authInfo, _) = + GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query); + return authInfo; + } + /// /// Gets the authorization. /// @@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security private AuthorizationInfo GetAuthorization(IRequest httpReq) { var auth = GetAuthorizationDictionary(httpReq); + var (authInfo, originalAuthInfo) = + GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString); + if (originalAuthInfo != null) + { + httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo; + } + + httpReq.Items["AuthorizationInfo"] = authInfo; + return authInfo; + } + + private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary( + in Dictionary auth, + in IHeaderDictionary headers, + in IQueryCollection queryString) + { string deviceId = null; string device = null; string client = null; @@ -64,19 +89,31 @@ namespace Emby.Server.Implementations.HttpServer.Security if (string.IsNullOrEmpty(token)) { - token = httpReq.Headers["X-Emby-Token"]; + token = headers["X-Jellyfin-Token"]; } if (string.IsNullOrEmpty(token)) { - token = httpReq.Headers["X-MediaBrowser-Token"]; - } - if (string.IsNullOrEmpty(token)) - { - token = httpReq.QueryString["api_key"]; + token = headers["X-Emby-Token"]; } - var info = new AuthorizationInfo + if (string.IsNullOrEmpty(token)) + { + token = headers["X-MediaBrowser-Token"]; + } + + if (string.IsNullOrEmpty(token)) + { + token = queryString["ApiKey"]; + } + + // TODO depricate this query parameter. + if (string.IsNullOrEmpty(token)) + { + token = queryString["api_key"]; + } + + var authInfo = new AuthorizationInfo { Client = client, Device = device, @@ -85,6 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security Token = token }; + AuthenticationInfo originalAuthenticationInfo = null; if (!string.IsNullOrWhiteSpace(token)) { var result = _authRepo.Get(new AuthenticationInfoQuery @@ -92,81 +130,77 @@ namespace Emby.Server.Implementations.HttpServer.Security AccessToken = token }); - var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null; + originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; - if (tokenInfo != null) + if (originalAuthenticationInfo != null) { var updateToken = false; // TODO: Remove these checks for IsNullOrWhiteSpace - if (string.IsNullOrWhiteSpace(info.Client)) + if (string.IsNullOrWhiteSpace(authInfo.Client)) { - info.Client = tokenInfo.AppName; + authInfo.Client = originalAuthenticationInfo.AppName; } - if (string.IsNullOrWhiteSpace(info.DeviceId)) + if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) { - info.DeviceId = tokenInfo.DeviceId; + authInfo.DeviceId = originalAuthenticationInfo.DeviceId; } // Temporary. TODO - allow clients to specify that the token has been shared with a casting device - var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; + var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; - if (string.IsNullOrWhiteSpace(info.Device)) + if (string.IsNullOrWhiteSpace(authInfo.Device)) { - info.Device = tokenInfo.DeviceName; + authInfo.Device = originalAuthenticationInfo.DeviceName; } - - else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) + else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) { if (allowTokenInfoUpdate) { updateToken = true; - tokenInfo.DeviceName = info.Device; + originalAuthenticationInfo.DeviceName = authInfo.Device; } } - if (string.IsNullOrWhiteSpace(info.Version)) + if (string.IsNullOrWhiteSpace(authInfo.Version)) { - info.Version = tokenInfo.AppVersion; + authInfo.Version = originalAuthenticationInfo.AppVersion; } - else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) + else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) { if (allowTokenInfoUpdate) { updateToken = true; - tokenInfo.AppVersion = info.Version; + originalAuthenticationInfo.AppVersion = authInfo.Version; } } - if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3) + if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3) { - tokenInfo.DateLastActivity = DateTime.UtcNow; + originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow; updateToken = true; } - if (!tokenInfo.UserId.Equals(Guid.Empty)) + if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) { - info.User = _userManager.GetUserById(tokenInfo.UserId); + authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); - if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase)) + if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) { - tokenInfo.UserName = info.User.Username; + originalAuthenticationInfo.UserName = authInfo.User.Username; updateToken = true; } } if (updateToken) { - _authRepo.Update(tokenInfo); + _authRepo.Update(originalAuthenticationInfo); } } - httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo; } - httpReq.Items["AuthorizationInfo"] = info; - - return info; + return (authInfo, originalAuthenticationInfo); } /// @@ -176,7 +210,32 @@ namespace Emby.Server.Implementations.HttpServer.Security /// Dictionary{System.StringSystem.String}. private Dictionary GetAuthorizationDictionary(IRequest httpReq) { - var auth = httpReq.Headers["X-Emby-Authorization"]; + var auth = httpReq.Headers["X-Jellyfin-Authorization"]; + if (string.IsNullOrEmpty(auth)) + { + auth = httpReq.Headers["X-Emby-Authorization"]; + } + + if (string.IsNullOrEmpty(auth)) + { + auth = httpReq.Headers[HeaderNames.Authorization]; + } + + return GetAuthorization(auth); + } + + /// + /// Gets the auth. + /// + /// The HTTP req. + /// Dictionary{System.StringSystem.String}. + private Dictionary GetAuthorizationDictionary(HttpRequest httpReq) + { + var auth = httpReq.Headers["X-Jellyfin-Authorization"]; + if (string.IsNullOrEmpty(auth)) + { + auth = httpReq.Headers["X-Emby-Authorization"]; + } if (string.IsNullOrEmpty(auth)) { @@ -206,7 +265,7 @@ namespace Emby.Server.Implementations.HttpServer.Security return null; } - var acceptedNames = new[] { "MediaBrowser", "Emby" }; + var acceptedNames = new[] { "MediaBrowser", "Emby", "Jellyfin" }; // It has to be a digest request if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase)) @@ -236,12 +295,7 @@ namespace Emby.Server.Implementations.HttpServer.Security private static string NormalizeValue(string value) { - if (string.IsNullOrEmpty(value)) - { - return value; - } - - return WebUtility.HtmlEncode(value); + return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value); } } } diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs index 61598391ff..37a7425b9d 100644 --- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -1,7 +1,11 @@ using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { + /// + /// IAuthorization context. + /// public interface IAuthorizationContext { /// @@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net /// The request context. /// AuthorizationInfo. AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); + + /// + /// Gets the authorization information. + /// + /// The request context. + /// AuthorizationInfo. + AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext); } } From a952d1567078dae0f5c732063e14a161cd784c0c Mon Sep 17 00:00:00 2001 From: David Date: Tue, 16 Jun 2020 16:08:31 +0200 Subject: [PATCH 194/724] Await Task from _libraryManager --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index ecbfed4693..a989efe7f1 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CA1801 using System; @@ -73,7 +74,7 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddVirtualFolder( + public async Task AddVirtualFolder( [FromQuery] string name, [FromQuery] string collectionType, [FromQuery] bool refreshLibrary, @@ -87,7 +88,7 @@ namespace Jellyfin.Api.Controllers libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); } - _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary); + await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); return NoContent(); } @@ -101,11 +102,11 @@ namespace Jellyfin.Api.Controllers /// A . [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveVirtualFolder( + public async Task RemoveVirtualFolder( [FromQuery] string name, [FromQuery] bool refreshLibrary) { - _libraryManager.RemoveVirtualFolder(name, refreshLibrary); + await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); return NoContent(); } From 774fdbd031f96dada757470c6e935f0667c775f1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 16 Jun 2020 14:12:40 -0600 Subject: [PATCH 195/724] Fix tests. --- .../Auth/CustomAuthenticationHandler.cs | 2 +- .../FirstTimeSetupOrElevatedHandler.cs | 1 + .../Auth/CustomAuthenticationHandlerTests.cs | 71 +++++------------ .../FirstTimeSetupOrElevatedHandlerTests.cs | 66 ++++++++++++++-- .../RequiresElevationHandlerTests.cs | 78 +++++++++++++++++-- .../Jellyfin.Api.Tests.csproj | 1 + 6 files changed, 155 insertions(+), 64 deletions(-) diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index d4d40da577..5e5e25e847 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -62,7 +62,7 @@ namespace Jellyfin.Api.Auth new Claim(InternalClaimTypes.Device, authorizationInfo.Device), new Claim(InternalClaimTypes.Client, authorizationInfo.Client), new Claim(InternalClaimTypes.Version, authorizationInfo.Version), - new Claim(InternalClaimTypes.Token, authorizationInfo.Token) + new Claim(InternalClaimTypes.Token, authorizationInfo.Token), }; var identity = new ClaimsIdentity(claims, Scheme.Name); diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index 0b12f7d3c2..decbe0c035 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -38,6 +38,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { context.Succeed(firstTimeSetupOrElevatedRequirement); + return Task.CompletedTask; } var validated = ValidateClaims(context.User); diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 362d41b015..4ea5094b66 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Security.Claims; -using System.Text.Encodings.Web; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; @@ -9,7 +8,6 @@ using Jellyfin.Api.Auth; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth private readonly IFixture _fixture; private readonly Mock _jellyfinAuthServiceMock; - private readonly Mock> _optionsMonitorMock; - private readonly Mock _clockMock; - private readonly Mock _serviceProviderMock; - private readonly Mock _authenticationServiceMock; - private readonly UrlEncoder _urlEncoder; - private readonly HttpContext _context; private readonly CustomAuthenticationHandler _sut; private readonly AuthenticationScheme _scheme; @@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth AllowFixtureCircularDependencies(); _jellyfinAuthServiceMock = _fixture.Freeze>(); - _optionsMonitorMock = _fixture.Freeze>>(); - _clockMock = _fixture.Freeze>(); - _serviceProviderMock = _fixture.Freeze>(); - _authenticationServiceMock = _fixture.Freeze>(); + var optionsMonitorMock = _fixture.Freeze>>(); + var serviceProviderMock = _fixture.Freeze>(); + var authenticationServiceMock = _fixture.Freeze>(); _fixture.Register(() => new NullLoggerFactory()); - _urlEncoder = UrlEncoder.Default; + serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService))) + .Returns(authenticationServiceMock.Object); - _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService))) - .Returns(_authenticationServiceMock.Object); - - _optionsMonitorMock.Setup(o => o.Get(It.IsAny())) + optionsMonitorMock.Setup(o => o.Get(It.IsAny())) .Returns(new AuthenticationSchemeOptions { ForwardAuthenticate = null }); - _context = new DefaultHttpContext + HttpContext context = new DefaultHttpContext { - RequestServices = _serviceProviderMock.Object + RequestServices = serviceProviderMock.Object }; _scheme = new AuthenticationScheme( @@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth typeof(CustomAuthenticationHandler)); _sut = _fixture.Create(); - _sut.InitializeAsync(_scheme, _context).Wait(); - } - - [Fact] - public async Task HandleAuthenticateAsyncShouldFailWithNullUser() - { - _jellyfinAuthServiceMock.Setup( - a => a.Authenticate( - It.IsAny(), - It.IsAny())) - .Returns((User?)null); - - var authenticateResult = await _sut.AuthenticateAsync(); - - Assert.False(authenticateResult.Succeeded); - Assert.True(authenticateResult.None); - // TODO return when legacy API is removed. - // Assert.Equal("Invalid user", authenticateResult.Failure.Message); + _sut.InitializeAsync(_scheme, context).Wait(); } [Fact] @@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth _jellyfinAuthServiceMock.Setup( a => a.Authenticate( - It.IsAny(), - It.IsAny())) + It.IsAny())) .Throws(new SecurityException(errorMessage)); var authenticateResult = await _sut.AuthenticateAsync(); @@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth [Fact] public async Task HandleAuthenticateAsyncShouldAssignNameClaim() { - var user = SetupUser(); + var authorizationInfo = SetupUser(); var authenticateResult = await _sut.AuthenticateAsync(); - Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username)); + Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username)); } [Theory] @@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth [InlineData(false)] public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin) { - var user = SetupUser(isAdmin); + var authorizationInfo = SetupUser(isAdmin); var authenticateResult = await _sut.AuthenticateAsync(); - var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User; + var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User; Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole)); } @@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme); } - private User SetupUser(bool isAdmin = false) + private AuthorizationInfo SetupUser(bool isAdmin = false) { - var user = _fixture.Create(); - user.SetPermission(PermissionKind.IsAdministrator, isAdmin); + var authorizationInfo = _fixture.Create(); + authorizationInfo.User = _fixture.Create(); + authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin); _jellyfinAuthServiceMock.Setup( a => a.Authenticate( - It.IsAny(), - It.IsAny())) - .Returns(user); + It.IsAny())) + .Returns(authorizationInfo); - return user; + return authorizationInfo; } private void AllowFixtureCircularDependencies() diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs index e40af703f9..e455df6435 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs @@ -1,13 +1,21 @@ +using System; using System.Collections.Generic; +using System.Globalization; +using System.Net; using System.Security.Claims; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Moq; using Xunit; @@ -15,15 +23,28 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy { public class FirstTimeSetupOrElevatedHandlerTests { + /// + /// 127.0.0.1. + /// + private const long InternalIp = 16777343; + + /// + /// 1.1.1.1. + /// + /// private const long ExternalIp = 16843009; private readonly Mock _configurationManagerMock; private readonly List _requirements; private readonly FirstTimeSetupOrElevatedHandler _sut; + private readonly Mock _userManagerMock; + private readonly Mock _httpContextAccessor; public FirstTimeSetupOrElevatedHandlerTests() { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze>(); _requirements = new List { new FirstTimeSetupOrElevatedRequirement() }; + _userManagerMock = fixture.Freeze>(); + _httpContextAccessor = fixture.Freeze>(); _sut = fixture.Create(); } @@ -35,8 +56,15 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole) { SetupConfigurationManager(false); - var user = SetupUser(userRole); - var context = new AuthorizationHandlerContext(_requirements, user, null); + var (user, claims) = SetupUser(userRole); + + _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) + .Returns(user); + + _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Returns(new IPAddress(InternalIp)); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.True(context.HasSucceeded); @@ -49,18 +77,42 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed) { SetupConfigurationManager(true); - var user = SetupUser(userRole); - var context = new AuthorizationHandlerContext(_requirements, user, null); + var (user, claims) = SetupUser(userRole); + + _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) + .Returns(user); + + _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Returns(new IPAddress(InternalIp)); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } - private static ClaimsPrincipal SetupUser(string role) + private static (User, ClaimsPrincipal) SetupUser(string role) { - var claims = new[] { new Claim(ClaimTypes.Role, role) }; + var user = new User( + "jellyfin", + typeof(DefaultAuthenticationProvider).FullName, + typeof(DefaultPasswordResetProvider).FullName); + + user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); + var claims = new[] + { + new Claim(ClaimTypes.Role, role), + new Claim(ClaimTypes.Name, "jellyfin"), + new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.Device, "test"), + new Claim(InternalClaimTypes.Client, "test"), + new Claim(InternalClaimTypes.Version, "test"), + new Claim(InternalClaimTypes.Token, "test"), + }; + var identity = new ClaimsIdentity(claims); - return new ClaimsPrincipal(identity); + return (user, new ClaimsPrincipal(identity)); } private void SetupConfigurationManager(bool startupWizardCompleted) diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs index cd05a8328d..58eae84789 100644 --- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs @@ -1,20 +1,48 @@ +using System; using System.Collections.Generic; +using System.Globalization; +using System.Net; using System.Security.Claims; using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations.Users; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Moq; using Xunit; namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy { public class RequiresElevationHandlerTests { + /// + /// 127.0.0.1. + /// + private const long InternalIp = 16777343; + + private readonly Mock _configurationManagerMock; + private readonly List _requirements; private readonly RequiresElevationHandler _sut; + private readonly Mock _userManagerMock; + private readonly Mock _httpContextAccessor; public RequiresElevationHandlerTests() { - _sut = new RequiresElevationHandler(); + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + _configurationManagerMock = fixture.Freeze>(); + _requirements = new List { new RequiresElevationRequirement() }; + _userManagerMock = fixture.Freeze>(); + _httpContextAccessor = fixture.Freeze>(); + + _sut = fixture.Create(); } [Theory] @@ -23,16 +51,54 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy [InlineData(UserRoles.Guest, false)] public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) { - var requirements = new List { new RequiresElevationRequirement() }; + SetupConfigurationManager(true); + var (user, claims) = SetupUser(role); - var claims = new[] { new Claim(ClaimTypes.Role, role) }; - var identity = new ClaimsIdentity(claims); - var user = new ClaimsPrincipal(identity); + _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) + .Returns(user); - var context = new AuthorizationHandlerContext(requirements, user, null); + _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Returns(new IPAddress(InternalIp)); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } + + private static (User, ClaimsPrincipal) SetupUser(string role) + { + var user = new User( + "jellyfin", + typeof(DefaultAuthenticationProvider).FullName, + typeof(DefaultPasswordResetProvider).FullName); + + user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); + var claims = new[] + { + new Claim(ClaimTypes.Role, role), + new Claim(ClaimTypes.Name, "jellyfin"), + new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.Device, "test"), + new Claim(InternalClaimTypes.Client, "test"), + new Claim(InternalClaimTypes.Version, "test"), + new Claim(InternalClaimTypes.Token, "test"), + }; + + var identity = new ClaimsIdentity(claims); + return (user, new ClaimsPrincipal(identity)); + } + + private void SetupConfigurationManager(bool startupWizardCompleted) + { + var commonConfiguration = new BaseApplicationConfiguration + { + IsStartupWizardCompleted = startupWizardCompleted + }; + + _configurationManagerMock.Setup(c => c.CommonConfiguration) + .Returns(commonConfiguration); + } } } diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index aedcc7c42e..010fad520a 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -35,6 +35,7 @@ + From c24666253c48ef17402bd8ddb7688821616ec6ba Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 16 Jun 2020 14:15:58 -0600 Subject: [PATCH 196/724] Add Default authorization policy --- Jellyfin.Api/Controllers/ConfigurationController.cs | 2 +- Jellyfin.Api/Controllers/DevicesController.cs | 2 +- Jellyfin.Api/Controllers/PackageController.cs | 2 +- Jellyfin.Api/Controllers/SearchController.cs | 3 ++- Jellyfin.Api/Controllers/SubtitleController.cs | 8 ++++---- Jellyfin.Api/Controllers/VideoAttachmentsController.cs | 3 ++- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 780a38aa81..5d37c9ade9 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -18,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// Configuration Controller. /// [Route("System")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 1754b0cbda..2f53628514 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers /// /// Devices Controller. /// - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 8200f891c8..b5b21fdeed 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -18,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// Package Controller. /// [Route("Packages")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class PackageController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index ec05e4fb4f..d971889db8 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.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers /// Search controller. /// [Route("/Search/Hints")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class SearchController : BaseJellyfinApiController { private readonly ISearchEngine _searchEngine; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 97df8c60d8..9aff359967 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -110,7 +110,7 @@ namespace Jellyfin.Api.Controllers /// Subtitles retrieved. /// An array of . [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( [FromRoute] Guid id, @@ -130,7 +130,7 @@ namespace Jellyfin.Api.Controllers /// Subtitle downloaded. /// A . [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( [FromRoute] Guid id, @@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers /// File returned. /// A with the subtitle file. [HttpGet("/Providers/Subtitles/Subtitles/{id}")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] public async Task GetRemoteSubtitles([FromRoute] string id) @@ -250,7 +250,7 @@ namespace Jellyfin.Api.Controllers /// Subtitle playlist retrieved. /// A with the HLS subtitle playlist. [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitlePlaylist( [FromRoute] Guid id, diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 86d9322fe4..32e26ff0be 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -4,6 +4,7 @@ using System; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -17,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// Attachments controller. /// [Route("Videos")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class VideoAttachmentsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; From b22fdbf59ee6536ca255ca3c57a13e5b9293fd78 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 16 Jun 2020 16:42:10 -0600 Subject: [PATCH 197/724] Add tests and cleanup --- .../DefaultAuthorizationHandlerTests.cs | 54 +++++++++++ .../FirstTimeSetupOrElevatedHandlerTests.cs | 80 +++------------- .../IgnoreScheduleHandlerTests.cs | 60 ++++++++++++ .../RequiresElevationHandlerTests.cs | 62 ++----------- tests/Jellyfin.Api.Tests/TestHelpers.cs | 92 +++++++++++++++++++ 5 files changed, 224 insertions(+), 124 deletions(-) create mode 100644 tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs create mode 100644 tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs create mode 100644 tests/Jellyfin.Api.Tests/TestHelpers.cs diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs new file mode 100644 index 0000000000..991ea3262f --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy +{ + public class DefaultAuthorizationHandlerTests + { + private readonly Mock _configurationManagerMock; + private readonly List _requirements; + private readonly DefaultAuthorizationHandler _sut; + private readonly Mock _userManagerMock; + private readonly Mock _httpContextAccessor; + + public DefaultAuthorizationHandlerTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + _configurationManagerMock = fixture.Freeze>(); + _requirements = new List { new DefaultAuthorizationRequirement() }; + _userManagerMock = fixture.Freeze>(); + _httpContextAccessor = fixture.Freeze>(); + + _sut = fixture.Create(); + } + + [Theory] + [InlineData(UserRoles.Administrator)] + [InlineData(UserRoles.Guest)] + [InlineData(UserRoles.User)] + public async Task ShouldSucceedOnUser(string userRole) + { + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + var (_, claims) = TestHelpers.SetupUser( + _userManagerMock, + _httpContextAccessor, + userRole, + TestHelpers.InternalIp); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); + + await _sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs index e455df6435..2b49419082 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs @@ -1,19 +1,11 @@ -using System; using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Security.Claims; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Constants; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Moq; @@ -23,15 +15,6 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy { public class FirstTimeSetupOrElevatedHandlerTests { - /// - /// 127.0.0.1. - /// - private const long InternalIp = 16777343; - - /// - /// 1.1.1.1. - /// - /// private const long ExternalIp = 16843009; private readonly Mock _configurationManagerMock; private readonly List _requirements; private readonly FirstTimeSetupOrElevatedHandler _sut; @@ -55,14 +38,12 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy [InlineData(UserRoles.User)] public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole) { - SetupConfigurationManager(false); - var (user, claims) = SetupUser(userRole); - - _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) - .Returns(user); - - _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) - .Returns(new IPAddress(InternalIp)); + TestHelpers.SetupConfigurationManager(_configurationManagerMock, false); + var (_, claims) = TestHelpers.SetupUser( + _userManagerMock, + _httpContextAccessor, + userRole, + TestHelpers.InternalIp); var context = new AuthorizationHandlerContext(_requirements, claims, null); @@ -76,54 +57,17 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy [InlineData(UserRoles.User, false)] public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed) { - SetupConfigurationManager(true); - var (user, claims) = SetupUser(userRole); - - _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) - .Returns(user); - - _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) - .Returns(new IPAddress(InternalIp)); + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + var (_, claims) = TestHelpers.SetupUser( + _userManagerMock, + _httpContextAccessor, + userRole, + TestHelpers.InternalIp); var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } - - private static (User, ClaimsPrincipal) SetupUser(string role) - { - var user = new User( - "jellyfin", - typeof(DefaultAuthenticationProvider).FullName, - typeof(DefaultPasswordResetProvider).FullName); - - user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); - var claims = new[] - { - new Claim(ClaimTypes.Role, role), - new Claim(ClaimTypes.Name, "jellyfin"), - new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), - new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), - new Claim(InternalClaimTypes.Device, "test"), - new Claim(InternalClaimTypes.Client, "test"), - new Claim(InternalClaimTypes.Version, "test"), - new Claim(InternalClaimTypes.Token, "test"), - }; - - var identity = new ClaimsIdentity(claims); - return (user, new ClaimsPrincipal(identity)); - } - - private void SetupConfigurationManager(bool startupWizardCompleted) - { - var commonConfiguration = new BaseApplicationConfiguration - { - IsStartupWizardCompleted = startupWizardCompleted - }; - - _configurationManagerMock.Setup(c => c.CommonConfiguration) - .Returns(commonConfiguration); - } } } diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs new file mode 100644 index 0000000000..25acfb581f --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Auth.IgnoreSchedulePolicy; +using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy +{ + public class IgnoreScheduleHandlerTests + { + private readonly Mock _configurationManagerMock; + private readonly List _requirements; + private readonly IgnoreScheduleHandler _sut; + private readonly Mock _userManagerMock; + private readonly Mock _httpContextAccessor; + + private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) }; + + public IgnoreScheduleHandlerTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + _configurationManagerMock = fixture.Freeze>(); + _requirements = new List { new IgnoreScheduleRequirement() }; + _userManagerMock = fixture.Freeze>(); + _httpContextAccessor = fixture.Freeze>(); + + _sut = fixture.Create(); + } + + [Theory] + [InlineData(UserRoles.Administrator, true)] + [InlineData(UserRoles.User, true)] + [InlineData(UserRoles.Guest, true)] + public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed) + { + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + var (_, claims) = TestHelpers.SetupUser( + _userManagerMock, + _httpContextAccessor, + role, + TestHelpers.InternalIp, + _accessSchedules); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); + + await _sut.HandleAsync(context); + Assert.Equal(shouldSucceed, context.HasSucceeded); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs index 58eae84789..f4617d0a42 100644 --- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs @@ -1,19 +1,11 @@ -using System; using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Security.Claims; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Moq; @@ -23,11 +15,6 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy { public class RequiresElevationHandlerTests { - /// - /// 127.0.0.1. - /// - private const long InternalIp = 16777343; - private readonly Mock _configurationManagerMock; private readonly List _requirements; private readonly RequiresElevationHandler _sut; @@ -51,54 +38,17 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy [InlineData(UserRoles.Guest, false)] public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) { - SetupConfigurationManager(true); - var (user, claims) = SetupUser(role); - - _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) - .Returns(user); - - _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) - .Returns(new IPAddress(InternalIp)); + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + var (_, claims) = TestHelpers.SetupUser( + _userManagerMock, + _httpContextAccessor, + role, + TestHelpers.InternalIp); var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } - - private static (User, ClaimsPrincipal) SetupUser(string role) - { - var user = new User( - "jellyfin", - typeof(DefaultAuthenticationProvider).FullName, - typeof(DefaultPasswordResetProvider).FullName); - - user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); - var claims = new[] - { - new Claim(ClaimTypes.Role, role), - new Claim(ClaimTypes.Name, "jellyfin"), - new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), - new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), - new Claim(InternalClaimTypes.Device, "test"), - new Claim(InternalClaimTypes.Client, "test"), - new Claim(InternalClaimTypes.Version, "test"), - new Claim(InternalClaimTypes.Token, "test"), - }; - - var identity = new ClaimsIdentity(claims); - return (user, new ClaimsPrincipal(identity)); - } - - private void SetupConfigurationManager(bool startupWizardCompleted) - { - var commonConfiguration = new BaseApplicationConfiguration - { - IsStartupWizardCompleted = startupWizardCompleted - }; - - _configurationManagerMock.Setup(c => c.CommonConfiguration) - .Returns(commonConfiguration); - } } } diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs new file mode 100644 index 0000000000..4617486fd9 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Security.Claims; +using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations.Users; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Http; +using Moq; +using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; + +namespace Jellyfin.Api.Tests +{ + public static class TestHelpers + { + /// + /// 127.0.0.1. + /// + public const long InternalIp = 16777343; + + /// + /// 1.1.1.1. + /// + public const long ExternalIp = 16843009; + + public static (User, ClaimsPrincipal) SetupUser( + Mock userManagerMock, + Mock httpContextAccessorMock, + string role, + long ip, + IEnumerable? accessSchedules = null) + { + var user = new User( + "jellyfin", + typeof(DefaultAuthenticationProvider).FullName, + typeof(DefaultPasswordResetProvider).FullName); + + // Set administrator flag. + user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); + + // Add access schedules if set. + if (accessSchedules != null) + { + foreach (var accessSchedule in accessSchedules) + { + user.AccessSchedules.Add(accessSchedule); + } + } + + var claims = new[] + { + new Claim(ClaimTypes.Role, role), + new Claim(ClaimTypes.Name, "jellyfin"), + new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.Device, "test"), + new Claim(InternalClaimTypes.Client, "test"), + new Claim(InternalClaimTypes.Version, "test"), + new Claim(InternalClaimTypes.Token, "test"), + }; + + var identity = new ClaimsIdentity(claims); + + userManagerMock + .Setup(u => u.GetUserById(It.IsAny())) + .Returns(user); + + httpContextAccessorMock + .Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Returns(new IPAddress(ip)); + + return (user, new ClaimsPrincipal(identity)); + } + + public static void SetupConfigurationManager(in Mock configurationManagerMock, bool startupWizardCompleted) + { + var commonConfiguration = new BaseApplicationConfiguration + { + IsStartupWizardCompleted = startupWizardCompleted + }; + + configurationManagerMock + .Setup(c => c.CommonConfiguration) + .Returns(commonConfiguration); + } + } +} From b451eb0bdc1594c88af11ae807fb7f3b3c4ef124 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 16 Jun 2020 16:45:17 -0600 Subject: [PATCH 198/724] Update Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs Co-authored-by: Patrick Barron <18354464+barronpm@users.noreply.github.com> --- .../HttpServer/Security/AuthorizationContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index deb9b5ebb0..b9fca67bf0 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.HttpServer.Security token = queryString["ApiKey"]; } - // TODO depricate this query parameter. + // TODO deprecate this query parameter. if (string.IsNullOrEmpty(token)) { token = queryString["api_key"]; From 29917699f0854f504452e62ee7be4bff0a4a206d Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 16 Jun 2020 16:55:02 -0600 Subject: [PATCH 199/724] Further cleanup and add final tests --- .../DefaultAuthorizationHandlerTests.cs | 5 +- .../FirstTimeSetupOrElevatedHandlerTests.cs | 10 ++-- .../IgnoreScheduleHandlerTests.cs | 6 +- .../LocalAccessHandlerTests.cs | 58 +++++++++++++++++++ .../RequiresElevationHandlerTests.cs | 5 +- tests/Jellyfin.Api.Tests/TestHelpers.cs | 17 +----- 6 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs index 991ea3262f..a62fd8d5ae 100644 --- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs @@ -39,11 +39,10 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy public async Task ShouldSucceedOnUser(string userRole) { TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var (_, claims) = TestHelpers.SetupUser( + var claims = TestHelpers.SetupUser( _userManagerMock, _httpContextAccessor, - userRole, - TestHelpers.InternalIp); + userRole); var context = new AuthorizationHandlerContext(_requirements, claims, null); diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs index 2b49419082..ee42216e46 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs @@ -39,11 +39,10 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole) { TestHelpers.SetupConfigurationManager(_configurationManagerMock, false); - var (_, claims) = TestHelpers.SetupUser( + var claims = TestHelpers.SetupUser( _userManagerMock, _httpContextAccessor, - userRole, - TestHelpers.InternalIp); + userRole); var context = new AuthorizationHandlerContext(_requirements, claims, null); @@ -58,11 +57,10 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed) { TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var (_, claims) = TestHelpers.SetupUser( + var claims = TestHelpers.SetupUser( _userManagerMock, _httpContextAccessor, - userRole, - TestHelpers.InternalIp); + userRole); var context = new AuthorizationHandlerContext(_requirements, claims, null); diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs index 25acfb581f..b65d45aa08 100644 --- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -24,6 +24,9 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy private readonly Mock _userManagerMock; private readonly Mock _httpContextAccessor; + /// + /// Globally disallow access. + /// private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) }; public IgnoreScheduleHandlerTests() @@ -44,11 +47,10 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed) { TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var (_, claims) = TestHelpers.SetupUser( + var claims = TestHelpers.SetupUser( _userManagerMock, _httpContextAccessor, role, - TestHelpers.InternalIp, _accessSchedules); var context = new AuthorizationHandlerContext(_requirements, claims, null); diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs new file mode 100644 index 0000000000..09ffa84689 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Auth.LocalAccessPolicy; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy +{ + public class LocalAccessHandlerTests + { + private readonly Mock _configurationManagerMock; + private readonly List _requirements; + private readonly LocalAccessHandler _sut; + private readonly Mock _userManagerMock; + private readonly Mock _httpContextAccessor; + private readonly Mock _networkManagerMock; + + public LocalAccessHandlerTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + _configurationManagerMock = fixture.Freeze>(); + _requirements = new List { new LocalAccessRequirement() }; + _userManagerMock = fixture.Freeze>(); + _httpContextAccessor = fixture.Freeze>(); + _networkManagerMock = fixture.Freeze>(); + + _sut = fixture.Create(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, false)] + public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed) + { + _networkManagerMock + .Setup(n => n.IsInLocalNetwork(It.IsAny())) + .Returns(isInLocalNetwork); + + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + var claims = TestHelpers.SetupUser( + _userManagerMock, + _httpContextAccessor, + UserRoles.User); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); + await _sut.HandleAsync(context); + Assert.Equal(shouldSucceed, context.HasSucceeded); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs index f4617d0a42..ffe88fcdeb 100644 --- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs @@ -39,11 +39,10 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) { TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var (_, claims) = TestHelpers.SetupUser( + var claims = TestHelpers.SetupUser( _userManagerMock, _httpContextAccessor, - role, - TestHelpers.InternalIp); + role); var context = new AuthorizationHandlerContext(_requirements, claims, null); diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index 4617486fd9..a4dd4e4092 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -18,21 +18,10 @@ namespace Jellyfin.Api.Tests { public static class TestHelpers { - /// - /// 127.0.0.1. - /// - public const long InternalIp = 16777343; - - /// - /// 1.1.1.1. - /// - public const long ExternalIp = 16843009; - - public static (User, ClaimsPrincipal) SetupUser( + public static ClaimsPrincipal SetupUser( Mock userManagerMock, Mock httpContextAccessorMock, string role, - long ip, IEnumerable? accessSchedules = null) { var user = new User( @@ -72,9 +61,9 @@ namespace Jellyfin.Api.Tests httpContextAccessorMock .Setup(h => h.HttpContext.Connection.RemoteIpAddress) - .Returns(new IPAddress(ip)); + .Returns(new IPAddress(0)); - return (user, new ClaimsPrincipal(identity)); + return new ClaimsPrincipal(identity); } public static void SetupConfigurationManager(in Mock configurationManagerMock, bool startupWizardCompleted) From 4962e230af13933f6a087b78b16884da0e485688 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 17 Jun 2020 06:52:15 -0600 Subject: [PATCH 200/724] revert adding Jellyfin to auth header --- .../Security/AuthorizationContext.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index b9fca67bf0..fb93fae3e6 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -87,11 +87,6 @@ namespace Emby.Server.Implementations.HttpServer.Security auth.TryGetValue("Token", out token); } - if (string.IsNullOrEmpty(token)) - { - token = headers["X-Jellyfin-Token"]; - } - if (string.IsNullOrEmpty(token)) { token = headers["X-Emby-Token"]; @@ -210,11 +205,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// Dictionary{System.StringSystem.String}. private Dictionary GetAuthorizationDictionary(IRequest httpReq) { - var auth = httpReq.Headers["X-Jellyfin-Authorization"]; - if (string.IsNullOrEmpty(auth)) - { - auth = httpReq.Headers["X-Emby-Authorization"]; - } + var auth = httpReq.Headers["X-Emby-Authorization"]; if (string.IsNullOrEmpty(auth)) { @@ -231,11 +222,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// Dictionary{System.StringSystem.String}. private Dictionary GetAuthorizationDictionary(HttpRequest httpReq) { - var auth = httpReq.Headers["X-Jellyfin-Authorization"]; - if (string.IsNullOrEmpty(auth)) - { - auth = httpReq.Headers["X-Emby-Authorization"]; - } + var auth = httpReq.Headers["X-Emby-Authorization"]; if (string.IsNullOrEmpty(auth)) { @@ -265,7 +252,7 @@ namespace Emby.Server.Implementations.HttpServer.Security return null; } - var acceptedNames = new[] { "MediaBrowser", "Emby", "Jellyfin" }; + var acceptedNames = new[] { "MediaBrowser", "Emby" }; // It has to be a digest request if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase)) From 0c01b6817b9e14661fd1ebea05590b60278e735c Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 17 Jun 2020 08:05:30 -0600 Subject: [PATCH 201/724] Add X-Forward-(For/Proto) support --- .../Extensions/ApiServiceCollectionExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 1ec77d716c..dbd5ba4166 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -18,6 +18,8 @@ using MediaBrowser.Common.Json; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -105,6 +107,10 @@ namespace Jellyfin.Server.Extensions { options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy); }) + .Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }) .AddMvc(opts => { opts.UseGeneralRoutePrefix(baseUrl); From f181cb374690a9c1af4abffe211ae5e44e4d63b3 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Wed, 17 Jun 2020 10:41:40 -0600 Subject: [PATCH 202/724] Update Jellyfin.Api/Controllers/FilterController.cs Co-authored-by: David --- Jellyfin.Api/Controllers/FilterController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 431114ea9a..46911ce938 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -159,10 +159,10 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); - if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || - string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) { parentItem = null; } From 0d1298e851d3cdfc56b74f44dc94cfc981a4e8f3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 17 Jun 2020 10:49:34 -0600 Subject: [PATCH 203/724] User proper File constructor --- .../Controllers/{Images => }/ImageByNameController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename Jellyfin.Api/Controllers/{Images => }/ImageByNameController.cs (96%) diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs similarity index 96% rename from Jellyfin.Api/Controllers/Images/ImageByNameController.cs rename to Jellyfin.Api/Controllers/ImageByNameController.cs index db475d6b47..fa46b6dd17 100644 --- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers.Images +namespace Jellyfin.Api.Controllers { /// /// Images By Name Controller. @@ -81,7 +81,7 @@ namespace Jellyfin.Api.Controllers.Images } var contentType = MimeTypes.GetMimeType(path); - return new FileStreamResult(System.IO.File.OpenRead(path), contentType); + return File(System.IO.File.OpenRead(path), contentType); } /// @@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers.Images if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { var contentType = MimeTypes.GetMimeType(path); - return new FileStreamResult(System.IO.File.OpenRead(path), contentType); + return File(System.IO.File.OpenRead(path), contentType); } } @@ -181,7 +181,7 @@ namespace Jellyfin.Api.Controllers.Images if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { var contentType = MimeTypes.GetMimeType(path); - return new FileStreamResult(System.IO.File.OpenRead(path), contentType); + return File(System.IO.File.OpenRead(path), contentType); } } From 9b45ee440cd2d167aee63a05bcbb6137765b4da8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 17 Jun 2020 10:51:50 -0600 Subject: [PATCH 204/724] User proper File constructor --- Jellyfin.Api/Controllers/Images/RemoteImageController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs index f521dfdf28..7c5f17e9e8 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs @@ -191,7 +191,7 @@ namespace Jellyfin.Api.Controllers.Images } var contentType = MimeTypes.GetMimeType(contentPath); - return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType); + return File(System.IO.File.OpenRead(contentPath), contentType); } /// From f2d7eac4189e99b587f6b7625820fb779d228ecc Mon Sep 17 00:00:00 2001 From: David Date: Wed, 17 Jun 2020 21:08:58 +0200 Subject: [PATCH 205/724] [WIP] Move UserService to Jellyfin.Api --- Jellyfin.Api/Controllers/UserController.cs | 424 +++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 Jellyfin.Api/Controllers/UserController.cs diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs new file mode 100644 index 0000000000..ff9373c2db --- /dev/null +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Users; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// User controller. + /// + [Route("/Users")] + public class UserController : BaseJellyfinApiController + { + private readonly IUserManager _userManager; + private readonly ISessionManager _sessionManager; + private readonly INetworkManager _networkManager; + private readonly IDeviceManager _deviceManager; + private readonly IAuthorizationContext _authContext; + private readonly IServerConfigurationManager _config; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public UserController( + IUserManager userManager, + ISessionManager sessionManager, + INetworkManager networkManager, + IDeviceManager deviceManager, + IAuthorizationContext authContext, + IServerConfigurationManager config) + { + _userManager = userManager; + _sessionManager = sessionManager; + _networkManager = networkManager; + _deviceManager = deviceManager; + _authContext = authContext; + _config = config; + } + + /// + /// Gets a list of users. + /// + /// Optional filter by IsHidden=true or false. + /// Optional filter by IsDisabled=true or false. + /// Optional filter by IsGuest=true or false. + /// + [HttpGet] + [Authorize] + public ActionResult> GetUsers( + [FromQuery] bool? isHidden, + [FromQuery] bool? isDisabled, + [FromQuery] bool? isGuest) + { + return Ok(Get(isHidden, isDisabled, isGuest, false, false)); + } + + /// + /// Gets a list of publicly visible users for display on a login screen. + /// + /// + [HttpGet("Public")] + public ActionResult> GetPublicUsers() + { + // If the startup wizard hasn't been completed then just return all users + if (!_config.Configuration.IsStartupWizardCompleted) + { + return GetUsers(null, false, null); + } + + return Ok(Get(false, false, false, true, true)); + } + + /// + /// Gets a user by Id. + /// + /// The user id. + /// + [HttpGet("{id}")] + // TODO: authorize escapeParentalControl + public ActionResult GetUserById([FromRoute] Guid id) + { + var user = _userManager.GetUserById(id); + + if (user == null) + { + throw new ResourceNotFoundException("User not found"); + } + + var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); + + return Ok(result); + } + + /// + /// Deletes a user. + /// + /// The user id. + /// A indicating success. + [HttpDelete("{id}")] + [Authorize(Policy = Policies.RequiresElevation)] + public ActionResult DeleteUser([FromRoute] Guid id) + { + var user = _userManager.GetUserById(id); + + if (user == null) + { + throw new ResourceNotFoundException("User not found"); + } + + _sessionManager.RevokeUserTokens(user.Id, null); + _userManager.DeleteUser(user); + return NoContent(); + } + + /// + /// Authenticates a user. + /// + /// The user id. + /// + /// + /// + [HttpPost("{id}/Authenticate")] + public async Task> AuthenticateUser( + [FromRoute, Required] Guid id, + [FromQuery, BindRequired] string pw, + [FromQuery, BindRequired] string password) + { + var user = _userManager.GetUserById(id); + + if (user == null) + { + return NotFound("User not found"); + } + + if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) + { + throw new MethodNotAllowedException(); + } + + // Password should always be null + return await AuthenticateUserByName(user.Username, null, pw).ConfigureAwait(false); + } + + /// + /// Authenticates a user by name. + /// + /// The username. + /// + /// + /// + [HttpPost("AuthenticateByName")] + public async Task> AuthenticateUserByName( + [FromQuery, BindRequired] string username, + [FromQuery, BindRequired] string pw, + [FromQuery, BindRequired] string password) + { + var auth = _authContext.GetAuthorizationInfo(Request); + + try + { + var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest + { + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device, + Password = pw, + PasswordSha1 = password, + RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), + Username = username + }).ConfigureAwait(false); + + return Ok(result); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); + } + } + + /// + /// Updates a user's password. + /// + /// + /// + /// + /// + /// Whether to reset the password. + /// A indicating success. + [HttpPost("{id}/Password")] + [Authorize] + public async Task UpdateUserPassword( + [FromRoute] Guid id, + [FromQuery] string currentPassword, + [FromQuery] string currentPw, + [FromQuery] string newPw, + [FromQuery] bool resetPassword) + { + AssertCanUpdateUser(_authContext, _userManager, id, true); + + var user = _userManager.GetUserById(id); + + if (user == null) + { + return NotFound("User not found"); + } + + if (resetPassword) + { + await _userManager.ResetPassword(user).ConfigureAwait(false); + } + else + { + var success = await _userManager.AuthenticateUser( + user.Username, + currentPw, + currentPassword, + HttpContext.Connection.RemoteIpAddress.ToString(), + false).ConfigureAwait(false); + + if (success == null) + { + throw new ArgumentException("Invalid user or password entered."); + } + + await _userManager.ChangePassword(user, newPw).ConfigureAwait(false); + + var currentToken = _authContext.GetAuthorizationInfo(Request).Token; + + _sessionManager.RevokeUserTokens(user.Id, currentToken); + } + + return NoContent(); + } + + /// + /// Updates a user's easy password. + /// + /// + /// + /// + /// + /// A indicating success. + [HttpPost("{id}/EasyPassword")] + [Authorize] + public ActionResult UpdateUserEasyPassword( + [FromRoute] Guid id, + [FromQuery] string newPassword, + [FromQuery] string newPw, + [FromQuery] bool resetPassword) + { + AssertCanUpdateUser(_authContext, _userManager, id, true); + + var user = _userManager.GetUserById(id); + + if (user == null) + { + return NotFound("User not found"); + } + + if (resetPassword) + { + _userManager.ResetEasyPassword(user); + } + else + { + _userManager.ChangeEasyPassword(user, newPw, newPassword); + } + + return NoContent(); + } + + /// + /// Updates a user. + /// + /// A indicating success. + [HttpPost("{id}")] + [Authorize] + public ActionResult UpdateUser() // TODO: missing UserDto + { + throw new NotImplementedException(); + } + + /// + /// Updates a user policy. + /// + /// The user id. + /// A indicating success. + [HttpPost("{id}/Policy")] + [Authorize] + public ActionResult UpdateUserPolicy([FromRoute] Guid id) // TODO: missing UserPolicy + { + throw new NotImplementedException(); + } + + /// + /// Updates a user configuration. + /// + /// The user id. + /// A indicating success. + [HttpPost("{id}/Configuration")] + [Authorize] + public ActionResult UpdateUserConfiguration([FromRoute] Guid id) // TODO: missing UserConfiguration + { + throw new NotImplementedException(); + } + + /// + /// Creates a user. + /// + /// The username. + /// The password. + /// A indicating success. + [HttpPost("/Users/New")] + [Authorize(Policy = Policies.RequiresElevation)] + public async Task CreateUserByName( + [FromBody] string name, + [FromBody] string password) + { + var newUser = _userManager.CreateUser(name); + + // no need to authenticate password for new user + if (password != null) + { + await _userManager.ChangePassword(newUser, password).ConfigureAwait(false); + } + + var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); + + return Ok(result); + } + + /// + /// Initiates the forgot password process for a local user. + /// + /// The entered username. + /// + [HttpPost("ForgotPassword")] + public async Task> ForgotPassword([FromBody] string enteredUsername) + { + var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress) + || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()); + + var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false); + + return Ok(result); + } + + /// + /// Redeems a forgot password pin. + /// + /// The pin. + /// + [HttpPost("ForgotPassword/Pin")] + public async Task> ForgotPasswordPin([FromBody] string pin) + { + var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); + return Ok(result); + } + + private IEnumerable Get(bool? isHidden, bool? isDisabled, bool? isGuest, bool filterByDevice, bool filterByNetwork) + { + var users = _userManager.Users; + + if (isDisabled.HasValue) + { + users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value); + } + + if (isHidden.HasValue) + { + users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value); + } + + if (filterByDevice) + { + var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId; + + if (!string.IsNullOrWhiteSpace(deviceId)) + { + users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId)); + } + } + + if (filterByNetwork) + { + if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString())) + { + users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); + } + } + + var result = users + .OrderBy(u => u.Username) + .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())) + .ToArray(); + + return result; + } + } +} From 9a51f484af3dbbb5717a88fb85473aec78234e32 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 18 Jun 2020 07:11:46 -0600 Subject: [PATCH 206/724] Remove nullable, add async task --- .../Controllers/ActivityLogController.cs | 1 - .../Controllers/ConfigurationController.cs | 2 -- Jellyfin.Api/Controllers/DevicesController.cs | 2 -- Jellyfin.Api/Controllers/FilterController.cs | 3 +-- .../Controllers/ImageByNameController.cs | 2 -- .../Controllers/ItemRefreshController.cs | 1 - .../Controllers/LibraryStructureController.cs | 25 +++++++------------ .../Controllers/NotificationsController.cs | 1 - Jellyfin.Api/Controllers/PackageController.cs | 2 -- Jellyfin.Api/Controllers/PluginsController.cs | 3 +-- .../{Images => }/RemoteImageController.cs | 4 +-- .../Controllers/SubtitleController.cs | 1 - .../Controllers/VideoAttachmentsController.cs | 2 -- .../ConfigurationDtos/MediaEncoderPathDto.cs | 2 -- .../NotificationDtos/NotificationDto.cs | 2 -- .../NotificationDtos/NotificationResultDto.cs | 2 -- .../NotificationsSummaryDto.cs | 2 -- .../Models/PluginDtos/MBRegistrationRecord.cs | 4 +-- .../Models/PluginDtos/PluginSecurityInfo.cs | 4 +-- .../StartupDtos/StartupConfigurationDto.cs | 8 +++--- .../Models/StartupDtos/StartupUserDto.cs | 6 ++--- 21 files changed, 19 insertions(+), 60 deletions(-) rename Jellyfin.Api/Controllers/{Images => }/RemoteImageController.cs (99%) diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index 895d9f719d..4ae7cf5069 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 780a38aa81..ae5685156e 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 1754b0cbda..1575307c55 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 46911ce938..6a6e6a64a3 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,5 +1,4 @@ -#nullable enable -#pragma warning disable CA1801 +#pragma warning disable CA1801 using System; using System.Linq; diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index fa46b6dd17..70f46ffa49 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index d9b8357d2e..a1df22e411 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System.ComponentModel; diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index a989efe7f1..ca2905b114 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System; @@ -175,20 +174,18 @@ namespace Jellyfin.Api.Controllers { CollectionFolder.OnCollectionFolderChange(); - Task.Run(() => + Task.Run(async () => { // No need to start if scanning the library because it will handle it if (refreshLibrary) { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } else { // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - + await Task.Delay(1000).ConfigureAwait(false); _libraryMonitor.Start(); } }); @@ -230,20 +227,18 @@ namespace Jellyfin.Api.Controllers } finally { - Task.Run(() => + Task.Run(async () => { // No need to start if scanning the library because it will handle it if (refreshLibrary) { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } else { // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - + await Task.Delay(1000).ConfigureAwait(false); _libraryMonitor.Start(); } }); @@ -304,20 +299,18 @@ namespace Jellyfin.Api.Controllers } finally { - Task.Run(() => + Task.Run(async () => { // No need to start if scanning the library because it will handle it if (refreshLibrary) { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } else { // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - + await Task.Delay(1000).ConfigureAwait(false); _libraryMonitor.Start(); } }); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index a76675d5a9..a1f9b9e8f7 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System; diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 8200f891c8..4f125f16b4 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 59196a41aa..fdb2f4c35b 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,5 +1,4 @@ -#nullable enable -#pragma warning disable CA1801 +#pragma warning disable CA1801 using System; using System.Collections.Generic; diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs similarity index 99% rename from Jellyfin.Api/Controllers/Images/RemoteImageController.cs rename to Jellyfin.Api/Controllers/RemoteImageController.cs index 7c5f17e9e8..80983ee649 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; @@ -21,7 +19,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Jellyfin.Api.Controllers.Images +namespace Jellyfin.Api.Controllers { /// /// Remote Images Controller. diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 97df8c60d8..caf30031ba 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System; diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 86d9322fe4..268aecad8a 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Net.Mime; using System.Threading; diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs index 3706a11e3a..3b827ec12d 100644 --- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs +++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Jellyfin.Api.Models.ConfigurationDtos { /// diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs index 502b22623b..af5239ec2b 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using MediaBrowser.Model.Notifications; diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs index e34e176cb9..64e92bd83a 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs index b3746ee2da..0568dea666 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs @@ -1,5 +1,3 @@ -#nullable enable - using MediaBrowser.Model.Notifications; namespace Jellyfin.Api.Models.NotificationDtos diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs index aaaf54267a..7f1255f4b6 100644 --- a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs +++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System; +using System; namespace Jellyfin.Api.Models.PluginDtos { diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs index 793002a6cd..a90398425a 100644 --- a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs +++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Jellyfin.Api.Models.PluginDtos +namespace Jellyfin.Api.Models.PluginDtos { /// /// Plugin security info. diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs index 5a83a030d2..a5f012245a 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace Jellyfin.Api.Models.StartupDtos { /// @@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos /// /// Gets or sets UI language culture. /// - public string UICulture { get; set; } + public string? UICulture { get; set; } /// /// Gets or sets the metadata country code. /// - public string MetadataCountryCode { get; set; } + public string? MetadataCountryCode { get; set; } /// /// Gets or sets the preferred language for the metadata. /// - public string PreferredMetadataLanguage { get; set; } + public string? PreferredMetadataLanguage { get; set; } } } diff --git a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs index 0dbb245ec6..e4c9735481 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace Jellyfin.Api.Models.StartupDtos { /// @@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos /// /// Gets or sets the username. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the user's password. /// - public string Password { get; set; } + public string? Password { get; set; } } } From 713ae7ae363cafd95bd93bfd69b4ac7ab5b9b32b Mon Sep 17 00:00:00 2001 From: David Date: Thu, 18 Jun 2020 18:09:58 +0200 Subject: [PATCH 207/724] Add xml comments; Add status codes; Use return instead of exception --- Jellyfin.Api/Controllers/UserController.cs | 260 ++++++-- Jellyfin.Api/Helpers/RequestHelpers.cs | 27 + .../Models/UserDtos/AuthenticateUserByName.cs | 9 + MediaBrowser.Api/UserService.cs | 605 ------------------ 4 files changed, 235 insertions(+), 666 deletions(-) create mode 100644 Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs delete mode 100644 MediaBrowser.Api/UserService.cs diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index ff9373c2db..825219c66a 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,11 +1,15 @@ -using System; +#nullable enable +#pragma warning disable CA1801 + +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; @@ -13,9 +17,11 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -65,51 +71,60 @@ namespace Jellyfin.Api.Controllers /// Optional filter by IsHidden=true or false. /// Optional filter by IsDisabled=true or false. /// Optional filter by IsGuest=true or false. - /// + /// Users returned. + /// An containing the users. [HttpGet] [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled, [FromQuery] bool? isGuest) { - return Ok(Get(isHidden, isDisabled, isGuest, false, false)); + var users = Get(isHidden, isDisabled, false, false); + return Ok(users); } /// /// Gets a list of publicly visible users for display on a login screen. /// - /// + /// Public users returned. + /// An containing the public users. [HttpGet("Public")] + [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPublicUsers() { // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { - return GetUsers(null, false, null); + return Ok(GetUsers(false, false, false).Value); } - return Ok(Get(false, false, false, true, true)); + return Ok(Get(false, false, true, true)); } /// /// Gets a user by Id. /// /// The user id. - /// + /// User returned. + /// User not found. + /// An with information about the user or a if the user was not found. [HttpGet("{id}")] // TODO: authorize escapeParentalControl + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid id) { var user = _userManager.GetUserById(id); if (user == null) { - throw new ResourceNotFoundException("User not found"); + return NotFound("User not found"); } var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); - return Ok(result); } @@ -117,16 +132,20 @@ namespace Jellyfin.Api.Controllers /// Deletes a user. /// /// The user id. - /// A indicating success. + /// User deleted. + /// User not found. + /// A indicating success or a if the user was not found. [HttpDelete("{id}")] [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteUser([FromRoute] Guid id) { var user = _userManager.GetUserById(id); if (user == null) { - throw new ResourceNotFoundException("User not found"); + return NotFound("User not found"); } _sessionManager.RevokeUserTokens(user.Id, null); @@ -138,10 +157,16 @@ namespace Jellyfin.Api.Controllers /// Authenticates a user. /// /// The user id. - /// - /// - /// + /// The password as plain text. + /// The password sha1-hash. + /// User authenticated. + /// Sha1-hashed password only is not allowed. + /// User not found. + /// A containing an . [HttpPost("{id}/Authenticate")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( [FromRoute, Required] Guid id, [FromQuery, BindRequired] string pw, @@ -156,25 +181,22 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) { - throw new MethodNotAllowedException(); + return Forbid("Only sha1 password is not allowed."); } // Password should always be null - return await AuthenticateUserByName(user.Username, null, pw).ConfigureAwait(false); + return await AuthenticateUserByName(user.Username, pw, password).ConfigureAwait(false); } /// /// Authenticates a user by name. /// - /// The username. - /// - /// - /// + /// The request. + /// User authenticated. + /// A containing an with information about the new session. [HttpPost("AuthenticateByName")] - public async Task> AuthenticateUserByName( - [FromQuery, BindRequired] string username, - [FromQuery, BindRequired] string pw, - [FromQuery, BindRequired] string password) + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request) { var auth = _authContext.GetAuthorizationInfo(Request); @@ -186,10 +208,10 @@ namespace Jellyfin.Api.Controllers AppVersion = auth.Version, DeviceId = auth.DeviceId, DeviceName = auth.Device, - Password = pw, - PasswordSha1 = password, + Password = request.Pw, + PasswordSha1 = request.Password, RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), - Username = username + Username = request.Username }).ConfigureAwait(false); return Ok(result); @@ -204,22 +226,31 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's password. /// - /// - /// - /// - /// + /// The user id. + /// The current password sha1-hash. + /// The current password as plain text. + /// The new password in plain text. /// Whether to reset the password. - /// A indicating success. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. [HttpPost("{id}/Password")] [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( [FromRoute] Guid id, - [FromQuery] string currentPassword, - [FromQuery] string currentPw, - [FromQuery] string newPw, - [FromQuery] bool resetPassword) + [FromBody] string currentPassword, + [FromBody] string currentPw, + [FromBody] string newPw, + [FromBody] bool resetPassword) { - AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + { + return Forbid("User is not allowed to update the password."); + } var user = _userManager.GetUserById(id); @@ -243,7 +274,7 @@ namespace Jellyfin.Api.Controllers if (success == null) { - throw new ArgumentException("Invalid user or password entered."); + return Forbid("Invalid user or password entered."); } await _userManager.ChangePassword(user, newPw).ConfigureAwait(false); @@ -259,20 +290,29 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's easy password. /// - /// - /// - /// - /// - /// A indicating success. + /// The user id. + /// The new password sha1-hash. + /// The new password in plain text. + /// Whether to reset the password. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. [HttpPost("{id}/EasyPassword")] [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateUserEasyPassword( [FromRoute] Guid id, - [FromQuery] string newPassword, - [FromQuery] string newPw, - [FromQuery] bool resetPassword) + [FromBody] string newPassword, + [FromBody] string newPw, + [FromBody] bool resetPassword) { - AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + { + return Forbid("User is not allowed to update the easy password."); + } var user = _userManager.GetUserById(id); @@ -296,36 +336,128 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user. /// - /// A indicating success. + /// The user id. + /// The updated user model. + /// User updated. + /// User information was not supplied. + /// User update forbidden. + /// A indicating success or a or a on failure. [HttpPost("{id}")] [Authorize] - public ActionResult UpdateUser() // TODO: missing UserDto + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task UpdateUser( + [FromRoute] Guid id, + [FromBody] UserDto updateUser) { - throw new NotImplementedException(); + if (updateUser == null) + { + return BadRequest(); + } + + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false)) + { + return Forbid("User update not allowed."); + } + + var user = _userManager.GetUserById(id); + + if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) + { + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + _userManager.UpdateConfiguration(user.Id, updateUser.Configuration); + } + else + { + await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); + _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration); + } + + return NoContent(); } /// /// Updates a user policy. /// /// The user id. - /// A indicating success. + /// The new user policy. + /// User policy updated. + /// User policy was not supplied. + /// User policy update forbidden. + /// A indicating success or a or a on failure.. [HttpPost("{id}/Policy")] [Authorize] - public ActionResult UpdateUserPolicy([FromRoute] Guid id) // TODO: missing UserPolicy + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult UpdateUserPolicy( + [FromRoute] Guid id, + [FromBody] UserPolicy newPolicy) { - throw new NotImplementedException(); + if (newPolicy == null) + { + return BadRequest(); + } + + var user = _userManager.GetUserById(id); + + // If removing admin access + if (!(newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))) + { + if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) + { + return Forbid("There must be at least one user in the system with administrative access."); + } + } + + // If disabling + if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) + { + return Forbid("Administrators cannot be disabled."); + } + + // If disabling + if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) + { + if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) + { + return Forbid("There must be at least one enabled user in the system."); + } + + var currentToken = _authContext.GetAuthorizationInfo(Request).Token; + _sessionManager.RevokeUserTokens(user.Id, currentToken); + } + + _userManager.UpdatePolicy(id, newPolicy); + + return NoContent(); } /// /// Updates a user configuration. /// /// The user id. + /// The new user configuration. + /// User configuration updated. + /// User configuration update forbidden. /// A indicating success. [HttpPost("{id}/Configuration")] [Authorize] - public ActionResult UpdateUserConfiguration([FromRoute] Guid id) // TODO: missing UserConfiguration + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult UpdateUserConfiguration( + [FromRoute] Guid id, + [FromBody] UserConfiguration userConfig) { - throw new NotImplementedException(); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false)) + { + return Forbid("User configuration update not allowed"); + } + + _userManager.UpdateConfiguration(id, userConfig); + + return NoContent(); } /// @@ -333,10 +465,12 @@ namespace Jellyfin.Api.Controllers /// /// The username. /// The password. - /// A indicating success. + /// User created. + /// An of the new user. [HttpPost("/Users/New")] [Authorize(Policy = Policies.RequiresElevation)] - public async Task CreateUserByName( + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateUserByName( [FromBody] string name, [FromBody] string password) { @@ -357,8 +491,10 @@ namespace Jellyfin.Api.Controllers /// Initiates the forgot password process for a local user. /// /// The entered username. - /// + /// Password reset process started. + /// A containing a . [HttpPost("ForgotPassword")] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPassword([FromBody] string enteredUsername) { var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress) @@ -373,15 +509,17 @@ namespace Jellyfin.Api.Controllers /// Redeems a forgot password pin. /// /// The pin. - /// + /// Pin reset process started. + /// A containing a . [HttpPost("ForgotPassword/Pin")] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPasswordPin([FromBody] string pin) { var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); return Ok(result); } - private IEnumerable Get(bool? isHidden, bool? isDisabled, bool? isGuest, bool filterByDevice, bool filterByNetwork) + private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { var users = _userManager.Users; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 9f4d34f9c6..6d6acbcf91 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,4 +1,7 @@ using System; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Helpers { @@ -25,5 +28,29 @@ namespace Jellyfin.Api.Helpers ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) : value.Split(separator); } + + /// + /// Checks if the user can update an entry. + /// + /// Instance of the interface. + /// The . + /// The user id. + /// Whether to restrict the user preferences. + /// A whether the user can update the entry. + internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences) + { + var auth = authContext.GetAuthorizationInfo(requestContext); + + var authenticatedUser = auth.User; + + // If they're going to update the record of another user, they must be an administrator + if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator)) + || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess)) + { + return false; + } + + return true; + } } } diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs new file mode 100644 index 0000000000..00b90a9250 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs @@ -0,0 +1,9 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + public class AuthenticateUserByName + { + public string Username { get; set; } + public string Pw { get; set; } + public string Password { get; set; } + } +} diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs deleted file mode 100644 index 9cb9baf631..0000000000 --- a/MediaBrowser.Api/UserService.cs +++ /dev/null @@ -1,605 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetUsers - /// - [Route("/Users", "GET", Summary = "Gets a list of users")] - [Authenticated] - public class GetUsers : IReturn - { - [ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsHidden { get; set; } - - [ApiMember(Name = "IsDisabled", Description = "Optional filter by IsDisabled=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsDisabled { get; set; } - - [ApiMember(Name = "IsGuest", Description = "Optional filter by IsGuest=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsGuest { get; set; } - } - - [Route("/Users/Public", "GET", Summary = "Gets a list of publicly visible users for display on a login screen.")] - public class GetPublicUsers : IReturn - { - } - - /// - /// Class GetUser - /// - [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")] - [Authenticated(EscapeParentalControl = true)] - public class GetUser : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - } - - /// - /// Class DeleteUser - /// - [Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")] - [Authenticated(Roles = "Admin")] - public class DeleteUser : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - } - - /// - /// Class AuthenticateUser - /// - [Route("/Users/{Id}/Authenticate", "POST", Summary = "Authenticates a user")] - public class AuthenticateUser : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - - [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Pw { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Password { get; set; } - } - - /// - /// Class AuthenticateUser - /// - [Route("/Users/AuthenticateByName", "POST", Summary = "Authenticates a user")] - public class AuthenticateUserByName : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Username", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Username { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Password { get; set; } - - [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Pw { get; set; } - } - - /// - /// Class UpdateUserPassword - /// - [Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")] - [Authenticated] - public class UpdateUserPassword : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - public Guid Id { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - public string CurrentPassword { get; set; } - - public string CurrentPw { get; set; } - - public string NewPw { get; set; } - - /// - /// Gets or sets a value indicating whether [reset password]. - /// - /// true if [reset password]; otherwise, false. - public bool ResetPassword { get; set; } - } - - /// - /// Class UpdateUserEasyPassword - /// - [Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")] - [Authenticated] - public class UpdateUserEasyPassword : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - public Guid Id { get; set; } - - /// - /// Gets or sets the new password. - /// - /// The new password. - public string NewPassword { get; set; } - - public string NewPw { get; set; } - - /// - /// Gets or sets a value indicating whether [reset password]. - /// - /// true if [reset password]; otherwise, false. - public bool ResetPassword { get; set; } - } - - /// - /// Class UpdateUser - /// - [Route("/Users/{Id}", "POST", Summary = "Updates a user")] - [Authenticated] - public class UpdateUser : UserDto, IReturnVoid - { - } - - /// - /// Class UpdateUser - /// - [Route("/Users/{Id}/Policy", "POST", Summary = "Updates a user policy")] - [Authenticated(Roles = "admin")] - public class UpdateUserPolicy : UserPolicy, IReturnVoid - { - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - } - - /// - /// Class UpdateUser - /// - [Route("/Users/{Id}/Configuration", "POST", Summary = "Updates a user configuration")] - [Authenticated] - public class UpdateUserConfiguration : UserConfiguration, IReturnVoid - { - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - } - - /// - /// Class CreateUser - /// - [Route("/Users/New", "POST", Summary = "Creates a user")] - [Authenticated(Roles = "Admin")] - public class CreateUserByName : IReturn - { - [ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Name { get; set; } - - [ApiMember(Name = "Password", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Password { get; set; } - } - - [Route("/Users/ForgotPassword", "POST", Summary = "Initiates the forgot password process for a local user")] - public class ForgotPassword : IReturn - { - [ApiMember(Name = "EnteredUsername", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")] - public string EnteredUsername { get; set; } - } - - [Route("/Users/ForgotPassword/Pin", "POST", Summary = "Redeems a forgot password pin")] - public class ForgotPasswordPin : IReturn - { - [ApiMember(Name = "Pin", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Pin { get; set; } - } - - /// - /// Class UsersService - /// - public class UserService : BaseApiService - { - /// - /// The user manager. - /// - private readonly IUserManager _userManager; - private readonly ISessionManager _sessionMananger; - private readonly INetworkManager _networkManager; - private readonly IDeviceManager _deviceManager; - private readonly IAuthorizationContext _authContext; - - public UserService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ISessionManager sessionMananger, - INetworkManager networkManager, - IDeviceManager deviceManager, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _userManager = userManager; - _sessionMananger = sessionMananger; - _networkManager = networkManager; - _deviceManager = deviceManager; - _authContext = authContext; - } - - public object Get(GetPublicUsers request) - { - // If the startup wizard hasn't been completed then just return all users - if (!ServerConfigurationManager.Configuration.IsStartupWizardCompleted) - { - return Get(new GetUsers - { - IsDisabled = false - }); - } - - return Get(new GetUsers - { - IsHidden = false, - IsDisabled = false - }, true, true); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetUsers request) - { - return Get(request, false, false); - } - - private object Get(GetUsers request, bool filterByDevice, bool filterByNetwork) - { - var users = _userManager.Users; - - if (request.IsDisabled.HasValue) - { - users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value); - } - - if (request.IsHidden.HasValue) - { - users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.IsHidden.Value); - } - - if (filterByDevice) - { - var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId; - - if (!string.IsNullOrWhiteSpace(deviceId)) - { - users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId)); - } - } - - if (filterByNetwork) - { - if (!_networkManager.IsInLocalNetwork(Request.RemoteIp)) - { - users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); - } - } - - var result = users - .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, Request.RemoteIp)) - .ToArray(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetUser request) - { - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - var result = _userManager.GetUserDto(user, Request.RemoteIp); - - return ToOptimizedResult(result); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public Task Delete(DeleteUser request) - { - return DeleteAsync(request); - } - - public Task DeleteAsync(DeleteUser request) - { - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - _sessionMananger.RevokeUserTokens(user.Id, null); - _userManager.DeleteUser(user); - return Task.CompletedTask; - } - - /// - /// Posts the specified request. - /// - /// The request. - public object Post(AuthenticateUser request) - { - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw)) - { - throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API."); - } - - // Password should always be null - return Post(new AuthenticateUserByName - { - Username = user.Username, - Password = null, - Pw = request.Pw - }); - } - - public async Task Post(AuthenticateUserByName request) - { - var auth = _authContext.GetAuthorizationInfo(Request); - - try - { - var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest - { - App = auth.Client, - AppVersion = auth.Version, - DeviceId = auth.DeviceId, - DeviceName = auth.Device, - Password = request.Pw, - PasswordSha1 = request.Password, - RemoteEndPoint = Request.RemoteIp, - Username = request.Username - }).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - catch (SecurityException e) - { - // rethrow adding IP address to message - throw new SecurityException($"[{Request.RemoteIp}] {e.Message}", e); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(UpdateUserPassword request) - { - return PostAsync(request); - } - - public async Task PostAsync(UpdateUserPassword request) - { - AssertCanUpdateUser(_authContext, _userManager, request.Id, true); - - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - if (request.ResetPassword) - { - await _userManager.ResetPassword(user).ConfigureAwait(false); - } - else - { - var success = await _userManager.AuthenticateUser( - user.Username, - request.CurrentPw, - request.CurrentPassword, - Request.RemoteIp, - false).ConfigureAwait(false); - - if (success == null) - { - throw new ArgumentException("Invalid user or password entered."); - } - - await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); - - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; - - _sessionMananger.RevokeUserTokens(user.Id, currentToken); - } - } - - public void Post(UpdateUserEasyPassword request) - { - AssertCanUpdateUser(_authContext, _userManager, request.Id, true); - - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - if (request.ResetPassword) - { - _userManager.ResetEasyPassword(user); - } - else - { - _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - public async Task Post(UpdateUser request) - { - var id = Guid.Parse(GetPathValue(1)); - - AssertCanUpdateUser(_authContext, _userManager, id, false); - - var dtoUser = request; - - var user = _userManager.GetUserById(id); - - if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal)) - { - await _userManager.UpdateUserAsync(user); - _userManager.UpdateConfiguration(user.Id, dtoUser.Configuration); - } - else - { - await _userManager.RenameUser(user, dtoUser.Name).ConfigureAwait(false); - - _userManager.UpdateConfiguration(dtoUser.Id, dtoUser.Configuration); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - /// System.Object. - public async Task Post(CreateUserByName request) - { - var newUser = _userManager.CreateUser(request.Name); - - // no need to authenticate password for new user - if (request.Password != null) - { - await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); - } - - var result = _userManager.GetUserDto(newUser, Request.RemoteIp); - - return ToOptimizedResult(result); - } - - public async Task Post(ForgotPassword request) - { - var isLocal = Request.IsLocal || _networkManager.IsInLocalNetwork(Request.RemoteIp); - - var result = await _userManager.StartForgotPasswordProcess(request.EnteredUsername, isLocal).ConfigureAwait(false); - - return result; - } - - public async Task Post(ForgotPasswordPin request) - { - var result = await _userManager.RedeemPasswordResetPin(request.Pin).ConfigureAwait(false); - - return result; - } - - public void Post(UpdateUserConfiguration request) - { - AssertCanUpdateUser(_authContext, _userManager, request.Id, false); - - _userManager.UpdateConfiguration(request.Id, request); - } - - public void Post(UpdateUserPolicy request) - { - var user = _userManager.GetUserById(request.Id); - - // If removing admin access - if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) - { - if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) - { - throw new ArgumentException("There must be at least one user in the system with administrative access."); - } - } - - // If disabling - if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) - { - throw new ArgumentException("Administrators cannot be disabled."); - } - - // If disabling - if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) - { - if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) - { - throw new ArgumentException("There must be at least one enabled user in the system."); - } - - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; - _sessionMananger.RevokeUserTokens(user.Id, currentToken); - } - - _userManager.UpdatePolicy(request.Id, request); - } - } -} From 69e1047bf30d672cf948e9feaae891e9934f6920 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 18 Jun 2020 10:42:48 -0600 Subject: [PATCH 208/724] Add DtoExtensions.cs --- Jellyfin.Api/Extensions/DtoExtensions.cs | 162 +++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 Jellyfin.Api/Extensions/DtoExtensions.cs diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs new file mode 100644 index 0000000000..4c587391fc --- /dev/null +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -0,0 +1,162 @@ +using System; +using System.Linq; +using Jellyfin.Api.Helpers; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Extensions +{ + /// + /// Dto Extensions. + /// + public static class DtoExtensions + { + /// + /// Add Dto Item fields. + /// + /// + /// Converted from IHasItemFields. + /// Legacy order: 1. + /// + /// DtoOptions object. + /// Comma delimited string of fields. + /// Modified DtoOptions object. + internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields) + { + if (string.IsNullOrEmpty(fields)) + { + dtoOptions.Fields = Array.Empty(); + } + else + { + dtoOptions.Fields = fields.Split(',') + .Select(v => + { + if (Enum.TryParse(v, true, out ItemFields value)) + { + return (ItemFields?)value; + } + + return null; + }) + .Where(i => i.HasValue) + .Select(i => i!.Value) + .ToArray(); + } + + return dtoOptions; + } + + /// + /// Add additional fields depending on client. + /// + /// + /// Use in place of GetDtoOptions. + /// Legacy order: 2. + /// + /// DtoOptions object. + /// Current request. + /// Modified DtoOptions object. + internal static DtoOptions AddClientFields( + this DtoOptions dtoOptions, HttpRequest request) + { + dtoOptions.Fields ??= Array.Empty(); + + string? client = ClaimHelpers.GetClient(request.HttpContext.User); + + // No client in claim + if (string.IsNullOrEmpty(client)) + { + return dtoOptions; + } + + if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount)) + { + if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) + { + int oldLen = dtoOptions.Fields.Length; + var arr = new ItemFields[oldLen + 1]; + dtoOptions.Fields.CopyTo(arr, 0); + arr[oldLen] = ItemFields.RecursiveItemCount; + dtoOptions.Fields = arr; + } + } + + if (!dtoOptions.ContainsField(ItemFields.ChildCount)) + { + if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) + { + int oldLen = dtoOptions.Fields.Length; + var arr = new ItemFields[oldLen + 1]; + dtoOptions.Fields.CopyTo(arr, 0); + arr[oldLen] = ItemFields.ChildCount; + dtoOptions.Fields = arr; + } + } + + return dtoOptions; + } + + /// + /// Add additional DtoOptions. + /// + /// + /// Converted from IHasDtoOptions. + /// Legacy order: 3. + /// + /// DtoOptions object. + /// Enable images. + /// Enable user data. + /// Image type limit. + /// Enable image types. + /// Modified DtoOptions object. + internal static DtoOptions AddAdditionalDtoOptions( + in DtoOptions dtoOptions, + bool? enableImages, + bool? enableUserData, + int? imageTypeLimit, + string enableImageTypes) + { + dtoOptions.EnableImages = enableImages ?? true; + + if (imageTypeLimit.HasValue) + { + dtoOptions.ImageTypeLimit = imageTypeLimit.Value; + } + + if (enableUserData.HasValue) + { + dtoOptions.EnableUserData = enableUserData.Value; + } + + if (!string.IsNullOrWhiteSpace(enableImageTypes)) + { + dtoOptions.ImageTypes = enableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)) + .ToArray(); + } + + return dtoOptions; + } + + /// + /// Check if DtoOptions contains field. + /// + /// DtoOptions object. + /// Field to check. + /// Field existence. + internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field) + => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field); + } +} From 77bea567082528be3d1da09ed214ec0a1e192a97 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 18 Jun 2020 19:35:29 +0200 Subject: [PATCH 209/724] Add request body models --- Jellyfin.Api/Controllers/UserController.cs | 54 ++++++++----------- .../Models/UserDtos/AuthenticateUserByName.cs | 20 +++++-- .../Models/UserDtos/CreateUserByName.cs | 18 +++++++ .../Models/UserDtos/UpdateUserEasyPassword.cs | 23 ++++++++ .../Models/UserDtos/UpdateUserPassword.cs | 28 ++++++++++ 5 files changed, 109 insertions(+), 34 deletions(-) create mode 100644 Jellyfin.Api/Models/UserDtos/CreateUserByName.cs create mode 100644 Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs create mode 100644 Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 825219c66a..24123085bf 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -111,8 +111,7 @@ namespace Jellyfin.Api.Controllers /// User not found. /// An with information about the user or a if the user was not found. [HttpGet("{id}")] - // TODO: authorize escapeParentalControl - [Authorize] + [Authorize(Policy = Policies.IgnoreSchedule)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid id) @@ -185,7 +184,13 @@ namespace Jellyfin.Api.Controllers } // Password should always be null - return await AuthenticateUserByName(user.Username, pw, password).ConfigureAwait(false); + AuthenticateUserByName request = new AuthenticateUserByName + { + Username = user.Username, + Password = null, + Pw = pw + }; + return await AuthenticateUserByName(request).ConfigureAwait(false); } /// @@ -227,10 +232,7 @@ namespace Jellyfin.Api.Controllers /// Updates a user's password. /// /// The user id. - /// The current password sha1-hash. - /// The current password as plain text. - /// The new password in plain text. - /// Whether to reset the password. + /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. @@ -242,10 +244,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( [FromRoute] Guid id, - [FromBody] string currentPassword, - [FromBody] string currentPw, - [FromBody] string newPw, - [FromBody] bool resetPassword) + [FromBody] UpdateUserPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) { @@ -259,7 +258,7 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - if (resetPassword) + if (request.ResetPassword) { await _userManager.ResetPassword(user).ConfigureAwait(false); } @@ -267,8 +266,8 @@ namespace Jellyfin.Api.Controllers { var success = await _userManager.AuthenticateUser( user.Username, - currentPw, - currentPassword, + request.CurrentPw, + request.CurrentPw, HttpContext.Connection.RemoteIpAddress.ToString(), false).ConfigureAwait(false); @@ -277,7 +276,7 @@ namespace Jellyfin.Api.Controllers return Forbid("Invalid user or password entered."); } - await _userManager.ChangePassword(user, newPw).ConfigureAwait(false); + await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); var currentToken = _authContext.GetAuthorizationInfo(Request).Token; @@ -291,9 +290,7 @@ namespace Jellyfin.Api.Controllers /// Updates a user's easy password. /// /// The user id. - /// The new password sha1-hash. - /// The new password in plain text. - /// Whether to reset the password. + /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. @@ -305,9 +302,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateUserEasyPassword( [FromRoute] Guid id, - [FromBody] string newPassword, - [FromBody] string newPw, - [FromBody] bool resetPassword) + [FromBody] UpdateUserEasyPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) { @@ -321,13 +316,13 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - if (resetPassword) + if (request.ResetPassword) { _userManager.ResetEasyPassword(user); } else { - _userManager.ChangeEasyPassword(user, newPw, newPassword); + _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); } return NoContent(); @@ -463,23 +458,20 @@ namespace Jellyfin.Api.Controllers /// /// Creates a user. /// - /// The username. - /// The password. + /// The create user by name request body. /// User created. /// An of the new user. [HttpPost("/Users/New")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateUserByName( - [FromBody] string name, - [FromBody] string password) + public async Task> CreateUserByName([FromBody] CreateUserByName request) { - var newUser = _userManager.CreateUser(name); + var newUser = _userManager.CreateUser(request.Name); // no need to authenticate password for new user - if (password != null) + if (request.Password != null) { - await _userManager.ChangePassword(newUser, password).ConfigureAwait(false); + await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs index 00b90a9250..3936274356 100644 --- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs +++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs @@ -1,9 +1,23 @@ namespace Jellyfin.Api.Models.UserDtos { + /// + /// The authenticate user by name request body. + /// public class AuthenticateUserByName { - public string Username { get; set; } - public string Pw { get; set; } - public string Password { get; set; } + /// + /// Gets or sets the username. + /// + public string? Username { get; set; } + + /// + /// Gets or sets the plain text password. + /// + public string? Pw { get; set; } + + /// + /// Gets or sets the sha1-hashed password. + /// + public string? Password { get; set; } } } diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs new file mode 100644 index 0000000000..1c88d36287 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The create user by name request body. + /// + public class CreateUserByName + { + /// + /// Gets or sets the username. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the password. + /// + public string? Password { get; set; } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs new file mode 100644 index 0000000000..0a173ea1a9 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs @@ -0,0 +1,23 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The update user easy password request body. + /// + public class UpdateUserEasyPassword + { + /// + /// Gets or sets the new sha1-hashed password. + /// + public string? NewPassword { get; set; } + + /// + /// Gets or sets the new password. + /// + public string? NewPw { get; set; } + + /// + /// Gets or sets a value indicating whether to reset the password. + /// + public bool ResetPassword { get; set; } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs new file mode 100644 index 0000000000..8288dbbc44 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs @@ -0,0 +1,28 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The update user password request body. + /// + public class UpdateUserPassword + { + /// + /// Gets or sets the current sha1-hashed password. + /// + public string? CurrentPassword { get; set; } + + /// + /// Gets or sets the current plain text password. + /// + public string? CurrentPw { get; set; } + + /// + /// Gets or sets the new plain text password. + /// + public string? NewPw { get; set; } + + /// + /// Gets or sets a value indicating whether to reset the password. + /// + public bool ResetPassword { get; set; } + } +} From 6651cb8d24f0de690b3be68db7c0b78e2413534f Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 12:24:39 +0200 Subject: [PATCH 210/724] Add JsonInto32Converter Add additional swagger type mapping --- .../ApiServiceCollectionExtensions.cs | 13 +++++++ .../Json/Converters/JsonInt32Converter.cs | 39 +++++++------------ MediaBrowser.Common/Json/JsonDefaults.cs | 1 + 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dbd5ba4166..821a52e476 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -215,6 +215,19 @@ namespace Jellyfin.Server.Extensions Format = "string" }) }); + + 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/JsonInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs index fe5dd6cd4d..70c375b8cd 100644 --- a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs @@ -14,40 +14,27 @@ namespace MediaBrowser.Common.Json.Converters /// public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - static void ThrowFormatException() => throw new FormatException("Invalid format for an integer."); - ReadOnlySpan span = stackalloc byte[0]; + if (reader.TokenType == JsonTokenType.String) + { + ReadOnlySpan span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed) + { + return number; + } - if (reader.HasValueSequence) - { - long sequenceLength = reader.ValueSequence.Length; - Span stackSpan = stackalloc byte[(int)sequenceLength]; - reader.ValueSequence.CopyTo(stackSpan); - span = stackSpan; - } - else - { - span = reader.ValueSpan; + if (int.TryParse(reader.GetString(), out number)) + { + return number; + } } - if (!Utf8Parser.TryParse(span, out int number, out _)) - { - ThrowFormatException(); - } - - return number; + return reader.GetInt32(); } /// public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) { - static void ThrowInvalidOperationException() => throw new InvalidOperationException(); - Span span = stackalloc byte[16]; - if (Utf8Formatter.TryFormat(value, span, out int bytesWritten)) - { - writer.WriteStringValue(span.Slice(0, bytesWritten)); - } - - ThrowInvalidOperationException(); + writer.WriteNumberValue(value); } } } diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index adc15123b1..ec3c45476c 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -28,6 +28,7 @@ namespace MediaBrowser.Common.Json }; options.Converters.Add(new JsonGuidConverter()); + options.Converters.Add(new JsonInt32Converter()); options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); From a5bd7f2d6ee0fef67c34f61db9be36167c30d890 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 13:03:53 +0200 Subject: [PATCH 211/724] Use new authorization and session functions --- Jellyfin.Api/Controllers/SessionController.cs | 32 ++++++++----------- Jellyfin.Api/Helpers/RequestHelpers.cs | 14 ++++++-- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 5b60275eb6..4f259536a1 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using Jellyfin.Api.Helpers; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -27,7 +28,6 @@ namespace Jellyfin.Api.Controllers private readonly IUserManager _userManager; private readonly IAuthorizationContext _authContext; private readonly IDeviceManager _deviceManager; - private readonly ISessionContext _sessionContext; /// /// Initializes a new instance of the class. @@ -36,19 +36,16 @@ namespace Jellyfin.Api.Controllers /// Instance of interface. /// Instance of interface. /// Instance of interface. - /// Instance of interface. public SessionController( ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext, - IDeviceManager deviceManager, - ISessionContext sessionContext) + IDeviceManager deviceManager) { _sessionManager = sessionManager; _userManager = userManager; _authContext = authContext; _deviceManager = deviceManager; - _sessionContext = sessionContext; } /// @@ -80,12 +77,12 @@ namespace Jellyfin.Api.Controllers var user = _userManager.GetUserById(controllableByUserId); - if (!user.Policy.EnableRemoteControlOfOtherUsers) + if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId)); } - if (!user.Policy.EnableSharedDeviceControl) + if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) { result = result.Where(i => !i.UserId.Equals(Guid.Empty)); } @@ -138,7 +135,7 @@ namespace Jellyfin.Api.Controllers }; _sessionManager.SendBrowseCommand( - RequestHelpers.GetSession(_sessionContext).Id, + RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, command, CancellationToken.None); @@ -175,7 +172,7 @@ namespace Jellyfin.Api.Controllers playRequest.PlayCommand = playCommand; _sessionManager.SendPlayCommand( - RequestHelpers.GetSession(_sessionContext).Id, + RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, playRequest, CancellationToken.None); @@ -197,7 +194,7 @@ namespace Jellyfin.Api.Controllers [FromBody] PlaystateRequest playstateRequest) { _sessionManager.SendPlaystateCommand( - RequestHelpers.GetSession(_sessionContext).Id, + RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, playstateRequest, CancellationToken.None); @@ -224,7 +221,7 @@ namespace Jellyfin.Api.Controllers name = commandType.ToString(); } - var currentSession = RequestHelpers.GetSession(_sessionContext); + var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); var generalCommand = new GeneralCommand { Name = name, @@ -249,7 +246,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] string id, [FromRoute] string command) { - var currentSession = RequestHelpers.GetSession(_sessionContext); + var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); var generalCommand = new GeneralCommand { @@ -275,7 +272,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] string id, [FromBody, Required] GeneralCommand command) { - var currentSession = RequestHelpers.GetSession(_sessionContext); + var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); if (command == null) { @@ -317,7 +314,7 @@ namespace Jellyfin.Api.Controllers Text = text }; - _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionContext).Id, id, command, CancellationToken.None); + _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, command, CancellationToken.None); return NoContent(); } @@ -379,7 +376,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionContext).Id; + id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; } _sessionManager.ReportCapabilities(id, new ClientCapabilities @@ -408,7 +405,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionContext).Id; + id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; } _sessionManager.ReportCapabilities(id, capabilities); @@ -429,7 +426,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string sessionId, [FromQuery] string itemId) { - string session = RequestHelpers.GetSession(_sessionContext).Id; + string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; _sessionManager.ReportNowViewingItem(session, itemId); return NoContent(); @@ -444,7 +441,6 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportSessionEnded() { - // TODO: how do we get AuthorizationInfo without an IRequest? AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request); _sessionManager.Logout(auth.Token); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index ae8ab37e8e..2aa700de3b 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,6 +1,7 @@ using System; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Helpers { @@ -28,10 +29,17 @@ namespace Jellyfin.Api.Helpers : value.Split(separator); } - internal static SessionInfo GetSession(ISessionContext sessionContext) + internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) { - // TODO: how do we get a SessionInfo without IRequest? - SessionInfo session = sessionContext.GetSession("Request"); + var authorization = authContext.GetAuthorizationInfo(request); + var user = authorization.User; + var session = sessionManager.LogSessionActivity( + authorization.Client, + authorization.Version, + authorization.DeviceId, + authorization.Device, + request.HttpContext.Connection.RemoteIpAddress.ToString(), + user); if (session == null) { From b51b9653ac9ff015d34233099bdc744fa153f8ee Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 14:29:32 +0200 Subject: [PATCH 212/724] Add missing authorization policies --- Jellyfin.Api/Controllers/SystemController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index cab6f308f0..f4dae40ef6 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -60,7 +60,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// A with info about the system. [HttpGet("Info")] - // TODO: Authorize EscapeParentalControl + [Authorize(Policy = Policies.IgnoreSchedule)] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetSystemInfo() @@ -99,7 +99,7 @@ namespace Jellyfin.Api.Controllers /// Server restarted. /// No content. Server restarted. [HttpPost("Restart")] - // TODO: Authorize AllowLocal = true + [Authorize(Policy = Policies.LocalAccessOnly)] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RestartApplication() From 08401f923dbe1bdf0086f905a6a9575b5ac9c5cd Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 15:49:44 +0200 Subject: [PATCH 213/724] Change swagger dictionary type mapping --- .../Extensions/ApiServiceCollectionExtensions.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 821a52e476..aad61d0429 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -216,6 +216,9 @@ namespace Jellyfin.Server.Extensions }) }); + /* + * Support BlurHash dictionary + */ options.MapType>>(() => new OpenApiSchema { @@ -224,8 +227,17 @@ namespace Jellyfin.Server.Extensions name => name, name => new OpenApiSchema { - Type = "string", - Format = "string" + Type = "object", Properties = new Dictionary + { + { + "string", + new OpenApiSchema + { + Type = "string", + Format = "string" + } + } + } }) }); } From d820c0fff50a79d85349660f507e820704d80d45 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 08:49:42 -0600 Subject: [PATCH 214/724] Convert pragma to supresswarning --- Jellyfin.Api/Controllers/ActivityLogController.cs | 4 ++-- Jellyfin.Api/Controllers/FilterController.cs | 6 +++--- Jellyfin.Api/Controllers/ItemRefreshController.cs | 4 ++-- .../Controllers/LibraryStructureController.cs | 4 ++-- Jellyfin.Api/Controllers/NotificationsController.cs | 12 ++++++++++-- Jellyfin.Api/Controllers/PluginsController.cs | 6 +++--- Jellyfin.Api/Controllers/SubtitleController.cs | 5 ++--- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index 4ae7cf5069..ec50fb022e 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,6 +1,5 @@ -#pragma warning disable CA1801 - using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; @@ -41,6 +40,7 @@ namespace Jellyfin.Api.Controllers /// A containing the log entries. [HttpGet("Entries")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")] public ActionResult> GetLogEntries( [FromQuery] int? startIndex, [FromQuery] int? limit, diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 6a6e6a64a3..dc5b0d9061 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,6 +1,5 @@ -#pragma warning disable CA1801 - -using System; +using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -137,6 +136,7 @@ namespace Jellyfin.Api.Controllers /// Query filters. [HttpGet("/Items/Filters2")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "mediaTypes", Justification = "Imported from ServiceStack")] public ActionResult GetQueryFilters( [FromQuery] Guid? userId, [FromQuery] string? parentId, diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index a1df22e411..6a16a89c5a 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -1,6 +1,5 @@ -#pragma warning disable CA1801 - using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; @@ -54,6 +53,7 @@ namespace Jellyfin.Api.Controllers [Description("Refreshes metadata for an item.")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")] public ActionResult Post( [FromRoute] string id, [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index ca2905b114..62c5474099 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,7 +1,6 @@ -#pragma warning disable CA1801 - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -56,6 +55,7 @@ namespace Jellyfin.Api.Controllers /// An with the virtual folders. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult> GetVirtualFolders([FromQuery] string userId) { return _libraryManager.GetVirtualFolders(true); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index a1f9b9e8f7..01dd23c77f 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,7 +1,6 @@ -#pragma warning disable CA1801 - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Jellyfin.Api.Models.NotificationDtos; @@ -45,6 +44,10 @@ namespace Jellyfin.Api.Controllers /// An containing a list of notifications. [HttpGet("{UserID}")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")] public ActionResult GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, @@ -62,6 +65,7 @@ namespace Jellyfin.Api.Controllers /// An containing a summary of the users notifications. [HttpGet("{UserID}/Summary")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult GetNotificationsSummary( [FromRoute] string userId) { @@ -136,6 +140,8 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] public ActionResult SetRead( [FromRoute] string userId, [FromQuery] string ids) @@ -152,6 +158,8 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] public ActionResult SetUnread( [FromRoute] string userId, [FromQuery] string ids) diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index fdb2f4c35b..6075544cf7 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,7 +1,6 @@ -#pragma warning disable CA1801 - -using System; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -46,6 +45,7 @@ namespace Jellyfin.Api.Controllers /// Installed plugins returned. /// List of currently installed plugins. [HttpGet] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")] public ActionResult> GetPlugins([FromRoute] bool? isAppStoreEnabled) { return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 69b83379d9..74ec5f9b52 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -1,8 +1,7 @@ -#pragma warning disable CA1801 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -251,9 +250,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task GetSubtitlePlaylist( [FromRoute] Guid id, - // TODO: 'int index' is never used: CA1801 is disabled [FromRoute] int index, [FromRoute] string mediaSourceId, [FromQuery, Required] int segmentLength) From 97ce641242e9df8475e01dd65a30aaf2ec763ffd Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 09:00:50 -0600 Subject: [PATCH 215/724] remove #nullable --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 0c5fece832..2537996512 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; From 6767c47ccdede27a374ea21b3013fe32ee356f01 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 09:01:37 -0600 Subject: [PATCH 216/724] remove #nullable --- Jellyfin.Api/Controllers/EnvironmentController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 046ffdf8eb..719bb7d86d 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; From 8f1505cdf5bc964c294cf0575807233f0e7af7d7 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 09:02:10 -0600 Subject: [PATCH 217/724] remove #nullable --- Jellyfin.Api/Controllers/ChannelsController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 6b42b500e0..a22cdd7803 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Linq; From e4a13f0e1e946ed306f2eb1be4217cde9d2622cb Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 09:02:41 -0600 Subject: [PATCH 218/724] remove #nullable --- Jellyfin.Api/Controllers/ScheduledTasksController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index e37e137d17..f7122c4134 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Linq; From d53a2899b1a82af1c64f3a2a558ae1ef201905ec Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 09:03:04 -0600 Subject: [PATCH 219/724] remove #nullable --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 35efe6b5f8..697a0baf42 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.ComponentModel.DataAnnotations; using System.Threading; using MediaBrowser.Controller.Persistence; From 68ea589f1af5bc5fdc656013d6e5d1deec99341f Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Fri, 19 Jun 2020 18:11:46 +0200 Subject: [PATCH 220/724] Use direct return instead of Ok() --- Jellyfin.Api/Controllers/UserController.cs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 24123085bf..68ab5813ce 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,9 +1,7 @@ -#nullable enable -#pragma warning disable CA1801 - -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -76,6 +74,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")] public ActionResult> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled, @@ -97,7 +96,7 @@ namespace Jellyfin.Api.Controllers // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { - return Ok(GetUsers(false, false, false).Value); + return Ok(Get(false, false, false, false)); } return Ok(Get(false, false, true, true)); @@ -124,7 +123,7 @@ namespace Jellyfin.Api.Controllers } var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); - return Ok(result); + return result; } /// @@ -219,7 +218,7 @@ namespace Jellyfin.Api.Controllers Username = request.Username }).ConfigureAwait(false); - return Ok(result); + return result; } catch (SecurityException e) { @@ -476,7 +475,7 @@ namespace Jellyfin.Api.Controllers var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); - return Ok(result); + return result; } /// @@ -494,7 +493,7 @@ namespace Jellyfin.Api.Controllers var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false); - return Ok(result); + return result; } /// @@ -508,7 +507,7 @@ namespace Jellyfin.Api.Controllers public async Task> ForgotPasswordPin([FromBody] string pin) { var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); - return Ok(result); + return result; } private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) @@ -545,8 +544,7 @@ namespace Jellyfin.Api.Controllers var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())) - .ToArray(); + .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())); return result; } From 7e91ded58792bb052ced4705cac08747ca2ea9d8 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Fri, 19 Jun 2020 18:20:49 +0200 Subject: [PATCH 221/724] Remove #nullable enable --- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 4 +--- Jellyfin.Api/Auth/CustomAuthenticationHandler.cs | 2 -- Jellyfin.Api/Helpers/ClaimHelpers.cs | 4 +--- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index b5b9d89041..953acac807 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Net; +using System.Net; using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 5e5e25e847..ea02e6a0b1 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Globalization; using System.Security.Authentication; using System.Security.Claims; diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs index a07d4ed820..df235ced25 100644 --- a/Jellyfin.Api/Helpers/ClaimHelpers.cs +++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System; +using System; using System.Linq; using System.Security.Claims; using Jellyfin.Api.Constants; From e2a7e8d97e26059d034e7c338adc0eb191642d80 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 13:10:10 -0600 Subject: [PATCH 222/724] Move LibraryService.cs to Jellyfin.Api --- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 11 +- .../Auth/DownloadPolicy/DownloadHandler.cs | 45 + .../DownloadPolicy/DownloadRequirement.cs | 11 + Jellyfin.Api/Constants/Policies.cs | 5 + Jellyfin.Api/Controllers/LibraryController.cs | 965 +++++++++++++- Jellyfin.Api/Helpers/RequestHelpers.cs | 18 + .../LibraryDtos/LibraryOptionInfoDto.cs | 18 + .../LibraryDtos/LibraryOptionsResultDto.cs | 34 + .../LibraryDtos/LibraryTypeOptionsDto.cs | 41 + .../Models/LibraryDtos/MediaUpdateInfoDto.cs | 19 + .../ApiServiceCollectionExtensions.cs | 9 + MediaBrowser.Api/Library/LibraryService.cs | 1116 ----------------- MediaBrowser.Api/MediaBrowser.Api.csproj | 4 + 13 files changed, 1178 insertions(+), 1118 deletions(-) create mode 100644 Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs create mode 100644 Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs create mode 100644 Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs create mode 100644 Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs create mode 100644 Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs create mode 100644 Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs delete mode 100644 MediaBrowser.Api/Library/LibraryService.cs diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index b5b9d89041..c66b841fae 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -44,11 +44,13 @@ namespace Jellyfin.Api.Auth /// Request claims. /// Whether to ignore parental control. /// Whether access is to be allowed locally only. + /// Whether validation requires download permission. /// Validated claim status. protected bool ValidateClaims( ClaimsPrincipal claimsPrincipal, bool ignoreSchedule = false, - bool localAccessOnly = false) + bool localAccessOnly = false, + bool requiredDownloadPermission = false) { // Ensure claim has userId. var userId = ClaimHelpers.GetUserId(claimsPrincipal); @@ -91,6 +93,13 @@ namespace Jellyfin.Api.Auth return false; } + // User attempting to download without permission. + if (requiredDownloadPermission + && !user.HasPermission(PermissionKind.EnableContentDownloading)) + { + return false; + } + return true; } diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs new file mode 100644 index 0000000000..fcfa55dfec --- /dev/null +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.DownloadPolicy +{ + /// + /// Download authorization handler. + /// + public class DownloadHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public DownloadHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement) + { + var validated = ValidateClaims(context.User); + if (validated) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs new file mode 100644 index 0000000000..b0a72a9dec --- /dev/null +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.DownloadPolicy +{ + /// + /// The download permission requirement. + /// + public class DownloadRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index cf574e43df..851b56d732 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -29,5 +29,10 @@ namespace Jellyfin.Api.Constants /// Policy name for escaping schedule controls. /// public const string IgnoreSchedule = "IgnoreSchedule"; + + /// + /// Policy name for requiring download permission. + /// + public const string Download = "Download"; } } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index f45101c0cb..e3e2e94894 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -1,10 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.LibraryDtos; +using Jellyfin.Data.Entities; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; +using Book = MediaBrowser.Controller.Entities.Book; +using Movie = Jellyfin.Data.Entities.Movie; +using MusicAlbum = Jellyfin.Data.Entities.MusicAlbum; namespace Jellyfin.Api.Controllers { @@ -21,6 +52,8 @@ namespace Jellyfin.Api.Controllers private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; private readonly ILibraryMonitor _libraryMonitor; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _serverConfigurationManager; /// /// Initializes a new instance of the class. @@ -33,6 +66,8 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. public LibraryController( IProviderManager providerManager, ILibraryManager libraryManager, @@ -41,7 +76,9 @@ namespace Jellyfin.Api.Controllers IAuthorizationContext authContext, IActivityManager activityManager, ILocalizationManager localization, - ILibraryMonitor libraryMonitor) + ILibraryMonitor libraryMonitor, + ILogger logger, + IServerConfigurationManager serverConfigurationManager) { _providerManager = providerManager; _libraryManager = libraryManager; @@ -51,6 +88,932 @@ namespace Jellyfin.Api.Controllers _activityManager = activityManager; _localization = localization; _libraryMonitor = libraryMonitor; + _logger = logger; + _serverConfigurationManager = serverConfigurationManager; + } + + /// + /// Get the original file of an item. + /// + /// The item id. + /// File stream returned. + /// Item not found. + /// A with the original file. + [HttpGet("/Items/{itemId}/File")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult GetFile([FromRoute] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + using var fileStream = new FileStream(item.Path, FileMode.Open, FileAccess.Read); + return File(fileStream, MimeTypes.GetMimeType(item.Path)); + } + + /// + /// Gets critic review for an item. + /// + /// The item id. + /// 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. + /// Critic reviews returned. + /// The list of critic reviews. + [HttpGet("/Items/{itemId}/CriticReviews")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")] + public ActionResult> GetCriticReviews( + [FromRoute] Guid itemId, + [FromQuery] int? startIndex, + [FromQuery] int? limit) + { + return new QueryResult(); + } + + /// + /// Get theme songs for an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. Determines whether or not parent items should be searched for theme media. + /// Theme songs returned. + /// Item not found. + /// The item theme songs. + [HttpGet("/Items/{itemId}/ThemeSongs")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult GetThemeSongs( + [FromRoute] Guid itemId, + [FromQuery] Guid userId, + [FromQuery] bool inheritFromParent) + { + var user = !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId) + : null; + + var item = itemId.Equals(Guid.Empty) + ? (!userId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.RootFolder) + : _libraryManager.GetItemById(itemId); + + if (item == null) + { + return NotFound("Item not found."); + } + + IEnumerable themeItems; + + while (true) + { + themeItems = item.GetThemeSongs(); + + if (themeItems.Any() || !inheritFromParent) + { + break; + } + + var parent = item.GetParent(); + if (parent == null) + { + break; + } + + item = parent; + } + + var dtoOptions = new DtoOptions().AddClientFields(Request); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); + + return new ThemeMediaResult + { + Items = items, + TotalRecordCount = items.Length, + OwnerId = item.Id + }; + } + + /// + /// Get theme videos for an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. Determines whether or not parent items should be searched for theme media. + /// Theme videos returned. + /// Item not found. + /// The item theme videos. + [HttpGet("/Items/{itemId}/ThemeVideos")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult GetThemeVideos( + [FromRoute] Guid itemId, + [FromQuery] Guid userId, + [FromQuery] bool inheritFromParent) + { + var user = !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId) + : null; + + var item = itemId.Equals(Guid.Empty) + ? (!userId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.RootFolder) + : _libraryManager.GetItemById(itemId); + + if (item == null) + { + return NotFound("Item not found."); + } + + IEnumerable themeItems; + + while (true) + { + themeItems = item.GetThemeVideos(); + + if (themeItems.Any() || !inheritFromParent) + { + break; + } + + var parent = item.GetParent(); + if (parent == null) + { + break; + } + + item = parent; + } + + var dtoOptions = new DtoOptions().AddClientFields(Request); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); + + return new ThemeMediaResult + { + Items = items, + TotalRecordCount = items.Length, + OwnerId = item.Id + }; + } + + /// + /// Get theme songs and videos for an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. Determines whether or not parent items should be searched for theme media. + /// Theme songs and videos returned. + /// Item not found. + /// The item theme videos. + public ActionResult GetThemeMedia( + [FromRoute] Guid itemId, + [FromQuery] Guid userId, + [FromQuery] bool inheritFromParent) + { + var themeSongs = GetThemeSongs( + itemId, + userId, + inheritFromParent); + + var themeVideos = GetThemeVideos( + itemId, + userId, + inheritFromParent); + + return new AllThemeMediaResult + { + ThemeSongsResult = themeSongs?.Value, + ThemeVideosResult = themeVideos?.Value, + SoundtrackSongsResult = new ThemeMediaResult() + }; + } + + /// + /// Starts a library scan. + /// + /// Library scan started. + /// A . + [HttpGet("/Library/Refresh")] + [Authorize(Policy = Policies.RequiresElevation)] + public async Task RefreshLibrary() + { + try + { + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing library"); + } + + return NoContent(); + } + + /// + /// Deletes an item from the library and filesystem. + /// + /// The item id. + /// Item deleted. + /// Unauthorized access. + /// A . + [HttpDelete("/Items/{itemId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult DeleteItem(Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + var auth = _authContext.GetAuthorizationInfo(Request); + var user = auth.User; + + if (!item.CanDelete(user)) + { + return Unauthorized("Unauthorized access"); + } + + _libraryManager.DeleteItem( + item, + new DeleteOptions { DeleteFileLocation = true }, + true); + + return NoContent(); + } + + /// + /// Deletes items from the library and filesystem. + /// + /// The item ids. + /// Items deleted. + /// Unauthorized access. + /// A . + [HttpDelete("/Items")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult DeleteItems([FromQuery] string ids) + { + var itemIds = string.IsNullOrWhiteSpace(ids) + ? Array.Empty() + : RequestHelpers.Split(ids, ',', true); + + foreach (var i in itemIds) + { + var item = _libraryManager.GetItemById(i); + var auth = _authContext.GetAuthorizationInfo(Request); + var user = auth.User; + + if (!item.CanDelete(user)) + { + if (ids.Length > 1) + { + return Unauthorized("Unauthorized access"); + } + + continue; + } + + _libraryManager.DeleteItem( + item, + new DeleteOptions { DeleteFileLocation = true }, + true); + } + + return NoContent(); + } + + /// + /// Get item counts. + /// + /// Optional. Get counts from a specific user's library. + /// Optional. Get counts of favorite items. + /// Item counts returned. + /// Item counts. + [HttpGet("/Items/Counts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult GetItemCounts( + [FromQuery] Guid userId, + [FromQuery] bool? isFavorite) + { + var user = userId.Equals(Guid.Empty) + ? null + : _userManager.GetUserById(userId); + + var counts = new ItemCounts + { + AlbumCount = GetCount(typeof(MusicAlbum), user, isFavorite), + EpisodeCount = GetCount(typeof(Episode), user, isFavorite), + MovieCount = GetCount(typeof(Movie), user, isFavorite), + SeriesCount = GetCount(typeof(Series), user, isFavorite), + SongCount = GetCount(typeof(Audio), user, isFavorite), + MusicVideoCount = GetCount(typeof(MusicVideo), user, isFavorite), + BoxSetCount = GetCount(typeof(BoxSet), user, isFavorite), + BookCount = GetCount(typeof(Book), user, isFavorite) + }; + + return counts; + } + + /// + /// Gets all parents of an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Item parents returned. + /// Item not found. + /// Item parents. + [HttpGet("/Items/{itemId}/Ancestors")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult> GetAncestors([FromRoute] Guid itemId, [FromQuery] Guid userId) + { + var item = _libraryManager.GetItemById(itemId); + + if (item == null) + { + return NotFound("Item not found"); + } + + var baseItemDtos = new List(); + + var user = !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId) + : null; + + var dtoOptions = new DtoOptions().AddClientFields(Request); + BaseItem parent = item.GetParent(); + + while (parent != null) + { + if (user != null) + { + parent = TranslateParentItem(parent, user); + } + + baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); + + parent = parent.GetParent(); + } + + return baseItemDtos; + } + + /// + /// Gets a list of physical paths from virtual folders. + /// + /// Physical paths returned. + /// List of physical paths. + [HttpGet("/Library/PhysicalPaths")] + [Authorize(Policy = Policies.RequiresElevation)] + public ActionResult> GetPhysicalPaths() + { + return Ok(_libraryManager.RootFolder.Children + .SelectMany(c => c.PhysicalLocations)); + } + + /// + /// Gets all user media folders. + /// + /// Optional. Filter by folders that are marked hidden, or not. + /// List of user media folders. + [HttpGet("/Library/MediaFolders")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult> GetMediaFolders([FromQuery] bool? isHidden) + { + var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); + + if (isHidden.HasValue) + { + var val = isHidden.Value; + + items = items.Where(i => i.IsHidden == val).ToList(); + } + + var dtoOptions = new DtoOptions().AddClientFields(Request); + var result = new QueryResult + { + TotalRecordCount = items.Count, + Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray() + }; + + return result; + } + + /// + /// Reports that new episodes of a series have been added by an external source. + /// + /// The tvdbId. + /// Report success. + /// A . + [HttpPost("/Library/Series/Added")] + [HttpPost("/Library/Series/Updated")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult PostUpdatedSeries([FromQuery] string tvdbId) + { + var series = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { nameof(Series) }, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray(); + + foreach (var item in series) + { + _libraryMonitor.ReportFileSystemChanged(item.Path); + } + + return NoContent(); + } + + /// + /// Reports that new movies have been added by an external source. + /// + /// The tmdbId. + /// The imdbId. + /// Report success. + /// A . + [HttpPost("/Library/Movies/Added")] + [HttpPost("/Library/Movies/Updated")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult PostUpdatedMovies([FromRoute] string tmdbId, [FromRoute] string imdbId) + { + var movies = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { nameof(Movie) }, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + }); + + if (!string.IsNullOrWhiteSpace(imdbId)) + { + movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList(); + } + else if (!string.IsNullOrWhiteSpace(tmdbId)) + { + movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList(); + } + else + { + movies = new List(); + } + + foreach (var item in movies) + { + _libraryMonitor.ReportFileSystemChanged(item.Path); + } + + return NoContent(); + } + + /// + /// Reports that new movies have been added by an external source. + /// + /// A list of updated media paths. + /// Report success. + /// A . + [HttpPost("/Library/Media/Updated")] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates) + { + foreach (var item in updates) + { + _libraryMonitor.ReportFileSystemChanged(item.Path); + } + + return NoContent(); + } + + /// + /// Downloads item media. + /// + /// The item id. + /// Media downloaded. + /// Item not found. + /// A containing the media stream. + /// User can't download or item can't be downloaded. + [HttpGet("/Items/{itemId}/Download")] + [Authorize(Policy = Policies.Download)] + public ActionResult GetDownload([FromRoute] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + var auth = _authContext.GetAuthorizationInfo(Request); + + var user = auth.User; + + if (user != null) + { + if (!item.CanDownload(user)) + { + throw new ArgumentException("Item does not support downloading"); + } + } + else + { + if (!item.CanDownload()) + { + throw new ArgumentException("Item does not support downloading"); + } + } + + if (user != null) + { + LogDownload(item, user, auth); + } + + var path = item.Path; + + // Quotes are valid in linux. They'll possibly cause issues here + var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty, StringComparison.Ordinal); + if (!string.IsNullOrWhiteSpace(filename)) + { + // Kestrel doesn't support non-ASCII characters in headers + if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]")) + { + // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2 + filename = WebUtility.UrlEncode(filename); + } + } + + // TODO determine non-ASCII validity. + using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); + return File(fileStream, MimeTypes.GetMimeType(path), filename); + } + + /// + /// Gets similar items. + /// + /// The item id. + /// Exclude artist ids. + /// (Unused) Optional. include image information in output. + /// (Unused) Optional. include user data. + /// (Unused) Optional. the max number of images to return, per image type. + /// (Unused) Optional. The image types to include in the output. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. + /// A containing the similar items. + [HttpGet("/Artists/{itemId}/Similar")] + [HttpGet("/Items/{itemId}/Similar")] + [HttpGet("/Albums/{itemId}/Similar")] + [HttpGet("/Shows/{itemId}/Similar")] + [HttpGet("/Movies/{itemId}/Similar")] + [HttpGet("/Trailers/{itemId}/Similar")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")] + public ActionResult> GetSimilarItems( + [FromRoute] Guid itemId, + [FromQuery] string excludeArtistIds, + [FromQuery] bool? enableImages, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery] string enableImageTypes, + [FromQuery] Guid userId, + [FromQuery] int? limit, + [FromQuery] string fields) + { + var item = itemId.Equals(Guid.Empty) + ? (!userId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.RootFolder) + : _libraryManager.GetItemById(itemId); + + var program = item as IHasProgramAttributes; + if (item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer) + { + /* + * // TODO + return new MoviesService( + _moviesServiceLogger, + ServerConfigurationManager, + ResultFactory, + _userManager, + _libraryManager, + _dtoService, + _authContext) + { + Request = Request, + }.GetSimilarItemsResult(request);*/ + } + + if (program != null && program.IsSeries) + { + return GetSimilarItemsResult( + item, + excludeArtistIds, + userId, + limit, + fields, + new[] { nameof(Series) }); + } + + if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist))) + { + return new QueryResult(); + } + + return GetSimilarItemsResult( + item, + excludeArtistIds, + userId, + limit, + fields, + new[] { item.GetType().Name }); + } + + /// + /// Gets the library options info. + /// + /// Library content type. + /// Whether this is a new library. + /// Library options info returned. + /// Library options info. + [HttpGet("/Libraries/AvailableOptions")] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + public ActionResult GetLibraryOptionsInfo([FromQuery] string libraryContentType, [FromQuery] bool isNewLibrary) + { + var result = new LibraryOptionsResultDto(); + + var types = GetRepresentativeItemTypes(libraryContentType); + var typesList = types.ToList(); + + var plugins = _providerManager.GetAllMetadataPlugins() + .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase)) + .OrderBy(i => typesList.IndexOf(i.ItemType)) + .ToList(); + + result.MetadataSavers = plugins + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary) + }) + .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .ToArray(); + + result.MetadataReaders = plugins + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = true + }) + .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .ToArray(); + + result.SubtitleFetchers = plugins + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = true + }) + .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .ToArray(); + + var typeOptions = new List(); + + foreach (var type in types) + { + TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions); + + typeOptions.Add(new LibraryTypeOptionsDto + { + Type = type, + + MetadataFetchers = plugins + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .ToArray(), + + ImageFetchers = plugins + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .ToArray(), + + SupportedImageTypes = plugins + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) + .Distinct() + .ToArray(), + + DefaultImageOptions = defaultImageOptions ?? Array.Empty() + }); + } + + result.TypeOptions = typeOptions.ToArray(); + + return result; + } + + private int GetCount(Type type, User? user, bool? isFavorite) + { + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { type.Name }, + Limit = 0, + Recursive = true, + IsVirtualItem = false, + IsFavorite = isFavorite, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + }; + + return _libraryManager.GetItemsResult(query).TotalRecordCount; + } + + private BaseItem TranslateParentItem(BaseItem item, User user) + { + return item.GetParent() is AggregateFolder + ? _libraryManager.GetUserRootFolder().GetChildren(user, true) + .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)) + : item; + } + + private void LogDownload(BaseItem item, User user, AuthorizationInfo auth) + { + try + { + _activityManager.Create(new ActivityLog( + string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), + "UserDownloadingContent", + auth.UserId) + { + ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), + }); + } + catch + { + // Logged at lower levels + } + } + + private QueryResult GetSimilarItemsResult( + BaseItem item, + string excludeArtistIds, + Guid userId, + int? limit, + string fields, + string[] includeItemTypes) + { + var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request); + + var query = new InternalItemsQuery(user) + { + Limit = limit, + IncludeItemTypes = includeItemTypes, + SimilarTo = item, + DtoOptions = dtoOptions, + EnableTotalRecordCount = false + }; + + // ExcludeArtistIds + if (!string.IsNullOrEmpty(excludeArtistIds)) + { + query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); + } + + List itemsResult; + + if (item is MusicArtist) + { + query.IncludeItemTypes = Array.Empty(); + + itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList(); + } + else + { + itemsResult = _libraryManager.GetItemList(query); + } + + var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); + + var result = new QueryResult + { + Items = returnList, + TotalRecordCount = itemsResult.Count + }; + + return result; + } + + private static string[] GetRepresentativeItemTypes(string contentType) + { + return contentType switch + { + CollectionType.BoxSets => new[] { "BoxSet" }, + CollectionType.Playlists => new[] { "Playlist" }, + CollectionType.Movies => new[] { "Movie" }, + CollectionType.TvShows => new[] { "Series", "Season", "Episode" }, + CollectionType.Books => new[] { "Book" }, + CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, + CollectionType.HomeVideos => new[] { "Video", "Photo" }, + CollectionType.Photos => new[] { "Video", "Photo" }, + CollectionType.MusicVideos => new[] { "MusicVideo" }, + _ => new[] { "Series", "Season", "Episode", "Movie" } + }; + } + + private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary) + { + if (isNewLibrary) + { + return false; + } + + var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions + .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase)); + } + + private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary) + { + if (isNewLibrary) + { + if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) + { + return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); + } + + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); + } + + var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + return metadataOptions.Length == 0 + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); + } + + private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) + { + if (isNewLibrary) + { + if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) + { + return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase); + } + + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); + } + + var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + if (metadataOptions.Length == 0) + { + return true; + } + + return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); } } } diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 9f4d34f9c6..a57cf146f6 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace Jellyfin.Api.Helpers { @@ -25,5 +26,22 @@ namespace Jellyfin.Api.Helpers ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) : value.Split(separator); } + + /// + /// Splits a comma delimited string and parses Guids. + /// + /// Input value. + /// Parsed Guids. + public static Guid[] GetGuids(string value) + { + if (value == null) + { + return Array.Empty(); + } + + return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(i => new Guid(i)) + .ToArray(); + } } } diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs new file mode 100644 index 0000000000..3584344344 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.LibraryDtos +{ + /// + /// Library option info dto. + /// + public class LibraryOptionInfoDto + { + /// + /// Gets or sets name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets a value indicating whether default enabled. + /// + public bool DefaultEnabled { get; set; } + } +} diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs new file mode 100644 index 0000000000..33eda33cb9 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Jellyfin.Api.Models.LibraryDtos +{ + /// + /// Library options result dto. + /// + public class LibraryOptionsResultDto + { + /// + /// Gets or sets the metadata savers. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataSavers", Justification = "Imported from ServiceStack")] + public LibraryOptionInfoDto[] MetadataSavers { get; set; } = null!; + + /// + /// Gets or sets the metadata readers. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataReaders", Justification = "Imported from ServiceStack")] + public LibraryOptionInfoDto[] MetadataReaders { get; set; } = null!; + + /// + /// Gets or sets the subtitle fetchers. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SubtitleFetchers", Justification = "Imported from ServiceStack")] + public LibraryOptionInfoDto[] SubtitleFetchers { get; set; } = null!; + + /// + /// Gets or sets the type options. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "TypeOptions", Justification = "Imported from ServiceStack")] + public LibraryTypeOptionsDto[] TypeOptions { get; set; } = null!; + } +} diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs new file mode 100644 index 0000000000..ad031e95e5 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; + +namespace Jellyfin.Api.Models.LibraryDtos +{ + /// + /// Library type options dto. + /// + public class LibraryTypeOptionsDto + { + /// + /// Gets or sets the type. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the metadata fetchers. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataFetchers", Justification = "Imported from ServiceStack")] + public LibraryOptionInfoDto[] MetadataFetchers { get; set; } = null!; + + /// + /// Gets or sets the image fetchers. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "ImageFetchers", Justification = "Imported from ServiceStack")] + public LibraryOptionInfoDto[] ImageFetchers { get; set; } = null!; + + /// + /// Gets or sets the supported image types. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SupportedImageTypes", Justification = "Imported from ServiceStack")] + public ImageType[] SupportedImageTypes { get; set; } = null!; + + /// + /// Gets or sets the default image options. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "DefaultImageOptions", Justification = "Imported from ServiceStack")] + public ImageOption[] DefaultImageOptions { get; set; } = null!; + } +} diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs new file mode 100644 index 0000000000..991dbfc502 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs @@ -0,0 +1,19 @@ +namespace Jellyfin.Api.Models.LibraryDtos +{ + /// + /// Media Update Info Dto. + /// + public class MediaUpdateInfoDto + { + /// + /// Gets or sets media path. + /// + public string? Path { get; set; } + + /// + /// Gets or sets media update type. + /// Created, Modified, Deleted. + /// + public string? UpdateType { get; set; } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dbd5ba4166..5f2fb7ea64 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using System.Reflection; using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using Jellyfin.Api.Auth.DownloadPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.IgnoreSchedulePolicy; using Jellyfin.Api.Auth.LocalAccessPolicy; @@ -39,6 +40,7 @@ namespace Jellyfin.Server.Extensions public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection) { serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -52,6 +54,13 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new DefaultAuthorizationRequirement()); }); + options.AddPolicy( + Policies.Download, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new DownloadRequirement()); + }); options.AddPolicy( Policies.FirstTimeSetupOrElevated, policy => diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs deleted file mode 100644 index e96875403d..0000000000 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ /dev/null @@ -1,1116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using MediaBrowser.Api.Movies; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using Book = MediaBrowser.Controller.Entities.Book; -using Episode = MediaBrowser.Controller.Entities.TV.Episode; -using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; -using Movie = MediaBrowser.Controller.Entities.Movies.Movie; -using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; -using Series = MediaBrowser.Controller.Entities.TV.Series; - -namespace MediaBrowser.Api.Library -{ - [Route("/Items/{Id}/File", "GET", Summary = "Gets the original file of an item")] - [Authenticated] - public class GetFile - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - /// - /// Class GetCriticReviews - /// - [Route("/Items/{Id}/CriticReviews", "GET", Summary = "Gets critic reviews for an item")] - [Authenticated] - public class GetCriticReviews : IReturn> - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - /// - /// 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; } - } - - /// - /// Class GetThemeSongs - /// - [Route("/Items/{Id}/ThemeSongs", "GET", Summary = "Gets theme songs for an item")] - [Authenticated] - public class GetThemeSongs : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public bool InheritFromParent { get; set; } - } - - /// - /// Class GetThemeVideos - /// - [Route("/Items/{Id}/ThemeVideos", "GET", Summary = "Gets theme videos for an item")] - [Authenticated] - public class GetThemeVideos : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public bool InheritFromParent { get; set; } - } - - /// - /// Class GetThemeVideos - /// - [Route("/Items/{Id}/ThemeMedia", "GET", Summary = "Gets theme videos and songs for an item")] - [Authenticated] - public class GetThemeMedia : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public bool InheritFromParent { get; set; } - } - - [Route("/Library/Refresh", "POST", Summary = "Starts a library scan")] - [Authenticated(Roles = "Admin")] - public class RefreshLibrary : IReturnVoid - { - } - - [Route("/Items/{Id}", "DELETE", Summary = "Deletes an item from the library and file system")] - [Authenticated] - public class DeleteItem : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - [Route("/Items", "DELETE", Summary = "Deletes an item from the library and file system")] - [Authenticated] - public class DeleteItems : IReturnVoid - { - [ApiMember(Name = "Ids", Description = "Ids", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string Ids { get; set; } - } - - [Route("/Items/Counts", "GET")] - [Authenticated] - public class GetItemCounts : IReturn - { - [ApiMember(Name = "UserId", Description = "Optional. Get counts from a specific user's library.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - [ApiMember(Name = "IsFavorite", Description = "Optional. Get counts of favorite items", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsFavorite { get; set; } - } - - [Route("/Items/{Id}/Ancestors", "GET", Summary = "Gets all parents of an item")] - [Authenticated] - public class GetAncestors : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - /// - /// Class GetPhyscialPaths - /// - [Route("/Library/PhysicalPaths", "GET", Summary = "Gets a list of physical paths from virtual folders")] - [Authenticated(Roles = "Admin")] - public class GetPhyscialPaths : IReturn> - { - } - - [Route("/Library/MediaFolders", "GET", Summary = "Gets all user media folders.")] - [Authenticated] - public class GetMediaFolders : IReturn> - { - [ApiMember(Name = "IsHidden", Description = "Optional. Filter by folders that are marked hidden, or not.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? IsHidden { get; set; } - } - - [Route("/Library/Series/Added", "POST", Summary = "Reports that new episodes of a series have been added by an external source")] - [Route("/Library/Series/Updated", "POST", Summary = "Reports that new episodes of a series have been added by an external source")] - [Authenticated] - public class PostUpdatedSeries : IReturnVoid - { - [ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")] - public string TvdbId { get; set; } - } - - [Route("/Library/Movies/Added", "POST", Summary = "Reports that new movies have been added by an external source")] - [Route("/Library/Movies/Updated", "POST", Summary = "Reports that new movies have been added by an external source")] - [Authenticated] - public class PostUpdatedMovies : IReturnVoid - { - [ApiMember(Name = "TmdbId", Description = "Tmdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")] - public string TmdbId { get; set; } - [ApiMember(Name = "ImdbId", Description = "Imdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")] - public string ImdbId { get; set; } - } - - public class MediaUpdateInfo - { - public string Path { get; set; } - - // Created, Modified, Deleted - public string UpdateType { get; set; } - } - - [Route("/Library/Media/Updated", "POST", Summary = "Reports that new movies have been added by an external source")] - [Authenticated] - public class PostUpdatedMedia : IReturnVoid - { - [ApiMember(Name = "Updates", Description = "A list of updated media paths", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")] - public List Updates { get; set; } - } - - [Route("/Items/{Id}/Download", "GET", Summary = "Downloads item media")] - [Authenticated(Roles = "download")] - public class GetDownload - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Artists/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")] - [Route("/Items/{Id}/Similar", "GET", Summary = "Gets similar items")] - [Route("/Albums/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")] - [Route("/Shows/{Id}/Similar", "GET", Summary = "Finds tv shows similar to a given one.")] - [Route("/Movies/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given movie.")] - [Route("/Trailers/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given trailer.")] - [Authenticated] - public class GetSimilarItems : BaseGetSimilarItemsFromItem - { - } - - [Route("/Libraries/AvailableOptions", "GET")] - [Authenticated(AllowBeforeStartupWizard = true)] - public class GetLibraryOptionsInfo : IReturn - { - public string LibraryContentType { get; set; } - public bool IsNewLibrary { get; set; } - } - - public class LibraryOptionInfo - { - public string Name { get; set; } - public bool DefaultEnabled { get; set; } - } - - public class LibraryOptionsResult - { - public LibraryOptionInfo[] MetadataSavers { get; set; } - public LibraryOptionInfo[] MetadataReaders { get; set; } - public LibraryOptionInfo[] SubtitleFetchers { get; set; } - public LibraryTypeOptions[] TypeOptions { get; set; } - } - - public class LibraryTypeOptions - { - public string Type { get; set; } - public LibraryOptionInfo[] MetadataFetchers { get; set; } - public LibraryOptionInfo[] ImageFetchers { get; set; } - public ImageType[] SupportedImageTypes { get; set; } - public ImageOption[] DefaultImageOptions { get; set; } - } - - /// - /// Class LibraryService - /// - public class LibraryService : BaseApiService - { - private readonly IProviderManager _providerManager; - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; - private readonly IAuthorizationContext _authContext; - private readonly IActivityManager _activityManager; - private readonly ILocalizationManager _localization; - private readonly ILibraryMonitor _libraryMonitor; - - private readonly ILogger _moviesServiceLogger; - - /// - /// Initializes a new instance of the class. - /// - public LibraryService( - ILogger logger, - ILogger moviesServiceLogger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IProviderManager providerManager, - ILibraryManager libraryManager, - IUserManager userManager, - IDtoService dtoService, - IAuthorizationContext authContext, - IActivityManager activityManager, - ILocalizationManager localization, - ILibraryMonitor libraryMonitor) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _providerManager = providerManager; - _libraryManager = libraryManager; - _userManager = userManager; - _dtoService = dtoService; - _authContext = authContext; - _activityManager = activityManager; - _localization = localization; - _libraryMonitor = libraryMonitor; - _moviesServiceLogger = moviesServiceLogger; - } - - // Content Types available for each Library - private string[] GetRepresentativeItemTypes(string contentType) - { - return contentType switch - { - CollectionType.BoxSets => new[] {"BoxSet"}, - CollectionType.Playlists => new[] {"Playlist"}, - CollectionType.Movies => new[] {"Movie"}, - CollectionType.TvShows => new[] {"Series", "Season", "Episode"}, - CollectionType.Books => new[] {"Book"}, - CollectionType.Music => new[] {"MusicArtist", "MusicAlbum", "Audio", "MusicVideo"}, - CollectionType.HomeVideos => new[] {"Video", "Photo"}, - CollectionType.Photos => new[] {"Video", "Photo"}, - CollectionType.MusicVideos => new[] {"MusicVideo"}, - _ => new[] {"Series", "Season", "Episode", "Movie"} - }; - } - - private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary) - { - if (isNewLibrary) - { - return false; - } - - var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions - .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - .ToArray(); - - if (metadataOptions.Length == 0) - { - return true; - } - - return metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase)); - } - - private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary) - { - if (isNewLibrary) - { - if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) - { - return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); - } - - return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); - } - - var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); - } - - private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) - { - if (isNewLibrary) - { - if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) - { - return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase) - && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) - && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) - && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase); - } - - return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); - } - - var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - if (metadataOptions.Length == 0) - { - return true; - } - - return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); - } - - public object Get(GetLibraryOptionsInfo request) - { - var result = new LibraryOptionsResult(); - - var types = GetRepresentativeItemTypes(request.LibraryContentType); - var isNewLibrary = request.IsNewLibrary; - var typesList = types.ToList(); - - var plugins = _providerManager.GetAllMetadataPlugins() - .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase)) - .OrderBy(i => typesList.IndexOf(i.ItemType)) - .ToList(); - - result.MetadataSavers = plugins - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver)) - .Select(i => new LibraryOptionInfo - { - Name = i.Name, - DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary) - }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) - .ToArray(); - - result.MetadataReaders = plugins - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider)) - .Select(i => new LibraryOptionInfo - { - Name = i.Name, - DefaultEnabled = true - }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) - .ToArray(); - - result.SubtitleFetchers = plugins - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher)) - .Select(i => new LibraryOptionInfo - { - Name = i.Name, - DefaultEnabled = true - }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) - .ToArray(); - - var typeOptions = new List(); - - foreach (var type in types) - { - TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions); - - typeOptions.Add(new LibraryTypeOptions - { - Type = type, - - MetadataFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) - .Select(i => new LibraryOptionInfo - { - Name = i.Name, - DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) - .ToArray(), - - ImageFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) - .Select(i => new LibraryOptionInfo - { - Name = i.Name, - DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) - .ToArray(), - - SupportedImageTypes = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) - .Distinct() - .ToArray(), - - DefaultImageOptions = defaultImageOptions ?? Array.Empty() - }); - } - - result.TypeOptions = typeOptions.ToArray(); - - return result; - } - - public object Get(GetSimilarItems request) - { - var item = string.IsNullOrEmpty(request.Id) ? - (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : - _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); - - var program = item as IHasProgramAttributes; - - if (item is Movie || (program != null && program.IsMovie) || item is Trailer) - { - return new MoviesService( - _moviesServiceLogger, - ServerConfigurationManager, - ResultFactory, - _userManager, - _libraryManager, - _dtoService, - _authContext) - { - Request = Request, - - }.GetSimilarItemsResult(request); - } - - if (program != null && program.IsSeries) - { - return GetSimilarItemsResult(request, new[] { typeof(Series).Name }); - } - - if (item is Episode || (item is IItemByName && !(item is MusicArtist))) - { - return new QueryResult(); - } - - return GetSimilarItemsResult(request, new[] { item.GetType().Name }); - } - - private QueryResult GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, string[] includeItemTypes) - { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var item = string.IsNullOrEmpty(request.Id) ? - (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : - _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); - - var dtoOptions = GetDtoOptions(_authContext, request); - - var query = new InternalItemsQuery(user) - { - Limit = request.Limit, - IncludeItemTypes = includeItemTypes, - SimilarTo = item, - DtoOptions = dtoOptions, - EnableTotalRecordCount = false - }; - - // ExcludeArtistIds - if (!string.IsNullOrEmpty(request.ExcludeArtistIds)) - { - query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds); - } - - List itemsResult; - - if (item is MusicArtist) - { - query.IncludeItemTypes = Array.Empty(); - - itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList(); - } - else - { - itemsResult = _libraryManager.GetItemList(query); - } - - var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); - - var result = new QueryResult - { - Items = returnList, - TotalRecordCount = itemsResult.Count - }; - - return result; - } - - public object Get(GetMediaFolders request) - { - var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); - - if (request.IsHidden.HasValue) - { - var val = request.IsHidden.Value; - - items = items.Where(i => i.IsHidden == val).ToList(); - } - - var dtoOptions = GetDtoOptions(_authContext, request); - - var result = new QueryResult - { - TotalRecordCount = items.Count, - - Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray() - }; - - return result; - } - - public void Post(PostUpdatedSeries request) - { - var series = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Series).Name }, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - - }).Where(i => string.Equals(request.TvdbId, i.GetProviderId(MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray(); - - foreach (var item in series) - { - _libraryMonitor.ReportFileSystemChanged(item.Path); - } - } - - public void Post(PostUpdatedMedia request) - { - if (request.Updates != null) - { - foreach (var item in request.Updates) - { - _libraryMonitor.ReportFileSystemChanged(item.Path); - } - } - } - - public void Post(PostUpdatedMovies request) - { - var movies = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Movie).Name }, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - - }); - - if (!string.IsNullOrWhiteSpace(request.ImdbId)) - { - movies = movies.Where(i => string.Equals(request.ImdbId, i.GetProviderId(MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList(); - } - else if (!string.IsNullOrWhiteSpace(request.TmdbId)) - { - movies = movies.Where(i => string.Equals(request.TmdbId, i.GetProviderId(MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList(); - } - else - { - movies = new List(); - } - - foreach (var item in movies) - { - _libraryMonitor.ReportFileSystemChanged(item.Path); - } - } - - public Task Get(GetDownload request) - { - var item = _libraryManager.GetItemById(request.Id); - var auth = _authContext.GetAuthorizationInfo(Request); - - var user = auth.User; - - if (user != null) - { - if (!item.CanDownload(user)) - { - throw new ArgumentException("Item does not support downloading"); - } - } - else - { - if (!item.CanDownload()) - { - throw new ArgumentException("Item does not support downloading"); - } - } - - var headers = new Dictionary(); - - if (user != null) - { - LogDownload(item, user, auth); - } - - var path = item.Path; - - // Quotes are valid in linux. They'll possibly cause issues here - var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty); - if (!string.IsNullOrWhiteSpace(filename)) - { - // Kestrel doesn't support non-ASCII characters in headers - if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]")) - { - // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2 - headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename); - } - else - { - headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\""; - } - } - - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - Path = path, - ResponseHeaders = headers - }); - } - - private void LogDownload(BaseItem item, User user, AuthorizationInfo auth) - { - try - { - _activityManager.Create(new ActivityLog( - string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), - "UserDownloadingContent", - auth.UserId) - { - ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), - }); - } - catch - { - // Logged at lower levels - } - } - - public Task Get(GetFile request) - { - var item = _libraryManager.GetItemById(request.Id); - - return ResultFactory.GetStaticFileResult(Request, item.Path); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPhyscialPaths request) - { - var result = _libraryManager.RootFolder.Children - .SelectMany(c => c.PhysicalLocations) - .ToList(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetAncestors request) - { - var result = GetAncestors(request); - - return ToOptimizedResult(result); - } - - /// - /// Gets the ancestors. - /// - /// The request. - /// Task{BaseItemDto[]}. - public List GetAncestors(GetAncestors request) - { - var item = _libraryManager.GetItemById(request.Id); - - var baseItemDtos = new List(); - - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var dtoOptions = GetDtoOptions(_authContext, request); - - BaseItem parent = item.GetParent(); - - while (parent != null) - { - if (user != null) - { - parent = TranslateParentItem(parent, user); - } - - baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); - - parent = parent.GetParent(); - } - - return baseItemDtos; - } - - private BaseItem TranslateParentItem(BaseItem item, User user) - { - return item.GetParent() is AggregateFolder - ? _libraryManager.GetUserRootFolder().GetChildren(user, true) - .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)) - : item; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetCriticReviews request) - { - return new QueryResult(); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetItemCounts request) - { - var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId); - - var counts = new ItemCounts - { - AlbumCount = GetCount(typeof(MusicAlbum), user, request), - EpisodeCount = GetCount(typeof(Episode), user, request), - MovieCount = GetCount(typeof(Movie), user, request), - SeriesCount = GetCount(typeof(Series), user, request), - SongCount = GetCount(typeof(Audio), user, request), - MusicVideoCount = GetCount(typeof(MusicVideo), user, request), - BoxSetCount = GetCount(typeof(BoxSet), user, request), - BookCount = GetCount(typeof(Book), user, request) - }; - - return ToOptimizedResult(counts); - } - - private int GetCount(Type type, User user, GetItemCounts request) - { - var query = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { type.Name }, - Limit = 0, - Recursive = true, - IsVirtualItem = false, - IsFavorite = request.IsFavorite, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - }; - - return _libraryManager.GetItemsResult(query).TotalRecordCount; - } - - /// - /// Posts the specified request. - /// - /// The request. - public async Task Post(RefreshLibrary request) - { - try - { - await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error refreshing library"); - } - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(DeleteItems request) - { - var ids = string.IsNullOrWhiteSpace(request.Ids) - ? Array.Empty() - : request.Ids.Split(','); - - foreach (var i in ids) - { - var item = _libraryManager.GetItemById(i); - var auth = _authContext.GetAuthorizationInfo(Request); - var user = auth.User; - - if (!item.CanDelete(user)) - { - if (ids.Length > 1) - { - throw new SecurityException("Unauthorized access"); - } - - continue; - } - - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = true - }, true); - } - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(DeleteItem request) - { - Delete(new DeleteItems - { - Ids = request.Id - }); - } - - public object Get(GetThemeMedia request) - { - var themeSongs = GetThemeSongs(new GetThemeSongs - { - InheritFromParent = request.InheritFromParent, - Id = request.Id, - UserId = request.UserId - - }); - - var themeVideos = GetThemeVideos(new GetThemeVideos - { - InheritFromParent = request.InheritFromParent, - Id = request.Id, - UserId = request.UserId - - }); - - return ToOptimizedResult(new AllThemeMediaResult - { - ThemeSongsResult = themeSongs, - ThemeVideosResult = themeVideos, - - SoundtrackSongsResult = new ThemeMediaResult() - }); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetThemeSongs request) - { - var result = GetThemeSongs(request); - - return ToOptimizedResult(result); - } - - private ThemeMediaResult GetThemeSongs(GetThemeSongs request) - { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var item = string.IsNullOrEmpty(request.Id) - ? (!request.UserId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) - : _libraryManager.GetItemById(request.Id); - - if (item == null) - { - throw new ResourceNotFoundException("Item not found."); - } - - IEnumerable themeItems; - - while (true) - { - themeItems = item.GetThemeSongs(); - - if (themeItems.Any() || !request.InheritFromParent) - { - break; - } - - var parent = item.GetParent(); - if (parent == null) - { - break; - } - item = parent; - } - - var dtoOptions = GetDtoOptions(_authContext, request); - var items = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) - .ToArray(); - - return new ThemeMediaResult - { - Items = items, - TotalRecordCount = items.Length, - OwnerId = item.Id - }; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetThemeVideos request) - { - return ToOptimizedResult(GetThemeVideos(request)); - } - - public ThemeMediaResult GetThemeVideos(GetThemeVideos request) - { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var item = string.IsNullOrEmpty(request.Id) - ? (!request.UserId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) - : _libraryManager.GetItemById(request.Id); - - if (item == null) - { - throw new ResourceNotFoundException("Item not found."); - } - - IEnumerable themeItems; - - while (true) - { - themeItems = item.GetThemeVideos(); - - if (themeItems.Any() || !request.InheritFromParent) - { - break; - } - - var parent = item.GetParent(); - if (parent == null) - { - break; - } - item = parent; - } - - var dtoOptions = GetDtoOptions(_authContext, request); - - var items = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) - .ToArray(); - - return new ThemeMediaResult - { - Items = items, - TotalRecordCount = items.Length, - OwnerId = item.Id - }; - } - } -} diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index d703bdb058..cd329c94f9 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -14,6 +14,10 @@ + + + + netstandard2.1 false From 45234e5ecd787c4d2e9aadae8459917f3baee045 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 13:29:14 -0600 Subject: [PATCH 223/724] Add movie support to existing GetSimilarItemsResult --- Jellyfin.Api/Controllers/LibraryController.cs | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index e3e2e94894..92843a3737 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -20,6 +20,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; @@ -690,23 +691,7 @@ namespace Jellyfin.Api.Controllers : _libraryManager.GetItemById(itemId); var program = item as IHasProgramAttributes; - if (item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer) - { - /* - * // TODO - return new MoviesService( - _moviesServiceLogger, - ServerConfigurationManager, - ResultFactory, - _userManager, - _libraryManager, - _dtoService, - _authContext) - { - Request = Request, - }.GetSimilarItemsResult(request);*/ - } - + var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer; if (program != null && program.IsSeries) { return GetSimilarItemsResult( @@ -715,7 +700,8 @@ namespace Jellyfin.Api.Controllers userId, limit, fields, - new[] { nameof(Series) }); + new[] { nameof(Series) }, + false); } if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist))) @@ -729,7 +715,8 @@ namespace Jellyfin.Api.Controllers userId, limit, fields, - new[] { item.GetType().Name }); + new[] { item.GetType().Name }, + isMovie); } /// @@ -885,7 +872,8 @@ namespace Jellyfin.Api.Controllers Guid userId, int? limit, string fields, - string[] includeItemTypes) + string[] includeItemTypes, + bool isMovie) { var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; var dtoOptions = new DtoOptions() @@ -896,9 +884,11 @@ namespace Jellyfin.Api.Controllers { Limit = limit, IncludeItemTypes = includeItemTypes, + IsMovie = isMovie, SimilarTo = item, DtoOptions = dtoOptions, - EnableTotalRecordCount = false + EnableTotalRecordCount = !isMovie, + EnableGroupByMetadataKey = isMovie }; // ExcludeArtistIds @@ -909,7 +899,19 @@ namespace Jellyfin.Api.Controllers List itemsResult; - if (item is MusicArtist) + if (isMovie) + { + var itemTypes = new List { nameof(MediaBrowser.Controller.Entities.Movies.Movie) }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(nameof(Trailer)); + itemTypes.Add(nameof(LiveTvProgram)); + } + + query.IncludeItemTypes = itemTypes.ToArray(); + itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList(); + } + else if (item is MusicArtist) { query.IncludeItemTypes = Array.Empty(); From 7c79ee0cd576ffe49f4268d70f4e63405fab9ea2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 13:51:01 -0600 Subject: [PATCH 224/724] Move MoviesService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/MoviesController.cs | 340 +++++++++++++++++++ MediaBrowser.Api/Movies/MoviesService.cs | 322 ------------------ 2 files changed, 340 insertions(+), 322 deletions(-) create mode 100644 Jellyfin.Api/Controllers/MoviesController.cs diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs new file mode 100644 index 0000000000..ef2de07e86 --- /dev/null +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Data.Entities; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Movies controller. + /// + [Authorize(Policy = Policies.DefaultAuthorization)] + public class MoviesController : BaseJellyfinApiController + { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public MoviesController( + IUserManager userManager, + ILibraryManager libraryManager, + IDtoService dtoService, + IServerConfigurationManager serverConfigurationManager) + { + _userManager = userManager; + _libraryManager = libraryManager; + _dtoService = dtoService; + _serverConfigurationManager = serverConfigurationManager; + } + + /// + /// Gets movie recommendations. + /// + /// Optional. Filter by user id, and attach user data. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// (Unused) Optional. include image information in output. + /// (Unused) Optional. include user data. + /// (Unused) Optional. the max number of images to return, per image type. + /// (Unused) Optional. The image types to include in the output. + /// Optional. The fields to return. + /// The max number of categories to return. + /// The max number of items to return per category. + /// Movie recommendations returned. + /// The list of movie recommendations. + [HttpGet("Recommendations")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")] + public ActionResult> GetMovieRecommendations( + [FromQuery] Guid userId, + [FromQuery] string parentId, + [FromQuery] bool? enableImages, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery] string enableImageTypes, + [FromQuery] string fields, + [FromQuery] int categoryLimit = 5, + [FromQuery] int itemLimit = 8) + { + var user = _userManager.GetUserById(userId); + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request); + + var categories = new List(); + + var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); + + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] + { + nameof(Movie), + // typeof(Trailer).Name, + // typeof(LiveTvProgram).Name + }, + // IsMovie = true + OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), + Limit = 7, + ParentId = parentIdGuid, + Recursive = true, + IsPlayed = true, + DtoOptions = dtoOptions + }; + + var recentlyPlayedMovies = _libraryManager.GetItemList(query); + + var itemTypes = new List { nameof(Movie) }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(nameof(Trailer)); + itemTypes.Add(nameof(LiveTvProgram)); + } + + var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), + Limit = 10, + IsFavoriteOrLiked = true, + ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), + EnableGroupByMetadataKey = true, + ParentId = parentIdGuid, + Recursive = true, + DtoOptions = dtoOptions + }); + + var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList(); + // Get recently played directors + var recentDirectors = GetDirectors(mostRecentMovies) + .ToList(); + + // Get recently played actors + var recentActors = GetActors(mostRecentMovies) + .ToList(); + + var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator(); + var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator(); + + var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator(); + var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator(); + + var categoryTypes = new List> + { + // Give this extra weight + similarToRecentlyPlayed, + similarToRecentlyPlayed, + + // Give this extra weight + similarToLiked, + similarToLiked, + hasDirectorFromRecentlyPlayed, + hasActorFromRecentlyPlayed + }; + + while (categories.Count < categoryLimit) + { + var allEmpty = true; + + foreach (var category in categoryTypes) + { + if (category.MoveNext()) + { + categories.Add(category.Current); + allEmpty = false; + + if (categories.Count >= categoryLimit) + { + break; + } + } + } + + if (allEmpty) + { + break; + } + } + + return Ok(categories.OrderBy(i => i.RecommendationType)); + } + + private IEnumerable GetWithDirector( + User user, + IEnumerable names, + int itemLimit, + DtoOptions dtoOptions, + RecommendationType type) + { + var itemTypes = new List { nameof(MediaBrowser.Controller.Entities.Movies.Movie) }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(nameof(Trailer)); + itemTypes.Add(nameof(LiveTvProgram)); + } + + foreach (var name in names) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Person = name, + // Account for duplicates by imdb id, since the database doesn't support this yet + Limit = itemLimit + 2, + PersonTypes = new[] { PersonType.Director }, + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) + .Select(x => x.First()) + .Take(itemLimit) + .ToList(); + + if (items.Count > 0) + { + var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); + + yield return new RecommendationDto + { + BaselineItemName = name, + CategoryId = name.GetMD5(), + RecommendationType = type, + Items = returnItems + }; + } + } + } + + private IEnumerable GetWithActor(User user, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + { + var itemTypes = new List { nameof(Movie) }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(nameof(Trailer)); + itemTypes.Add(nameof(LiveTvProgram)); + } + + foreach (var name in names) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Person = name, + // Account for duplicates by imdb id, since the database doesn't support this yet + Limit = itemLimit + 2, + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) + .Select(x => x.First()) + .Take(itemLimit) + .ToList(); + + if (items.Count > 0) + { + var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); + + yield return new RecommendationDto + { + BaselineItemName = name, + CategoryId = name.GetMD5(), + RecommendationType = type, + Items = returnItems + }; + } + } + } + + private IEnumerable GetSimilarTo(User user, IEnumerable baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + { + var itemTypes = new List { nameof(Movie) }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(nameof(Trailer)); + itemTypes.Add(nameof(LiveTvProgram)); + } + + foreach (var item in baselineItems) + { + var similar = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Limit = itemLimit, + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + SimilarTo = item, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }); + + if (similar.Count > 0) + { + var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user); + + yield return new RecommendationDto + { + BaselineItemName = item.Name, + CategoryId = item.Id, + RecommendationType = type, + Items = returnItems + }; + } + } + } + + private IEnumerable GetActors(IEnumerable items) + { + var people = _libraryManager.GetPeople(new InternalPeopleQuery + { + ExcludePersonTypes = new[] { PersonType.Director }, + MaxListOrder = 3 + }); + + var itemIds = items.Select(i => i.Id).ToList(); + + return people + .Where(i => itemIds.Contains(i.ItemId)) + .Select(i => i.Name) + .DistinctNames(); + } + + private IEnumerable GetDirectors(IEnumerable items) + { + var people = _libraryManager.GetPeople(new InternalPeopleQuery + { + PersonTypes = new[] { PersonType.Director } + }); + + var itemIds = items.Select(i => i.Id).ToList(); + + return people + .Where(i => itemIds.Contains(i.ItemId)) + .Select(i => i.Name) + .DistinctNames(); + } + } +} diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 2d61299c76..1931914d2b 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -20,50 +20,6 @@ using Movie = MediaBrowser.Controller.Entities.Movies.Movie; namespace MediaBrowser.Api.Movies { - [Route("/Movies/Recommendations", "GET", Summary = "Gets movie recommendations")] - public class GetMovieRecommendations : IReturn, IHasDtoOptions - { - [ApiMember(Name = "CategoryLimit", Description = "The max number of categories to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int CategoryLimit { get; set; } - - [ApiMember(Name = "ItemLimit", Description = "The max number of items to return per category", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int ItemLimit { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// - /// The parent id. - [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ParentId { get; set; } - - [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? EnableImages { get; set; } - - [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? EnableUserData { get; set; } - - [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? ImageTypeLimit { get; set; } - - [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string EnableImageTypes { get; set; } - - public GetMovieRecommendations() - { - CategoryLimit = 5; - ItemLimit = 8; - } - - public string Fields { get; set; } - } - /// /// Class MoviesService /// @@ -74,9 +30,7 @@ namespace MediaBrowser.Api.Movies /// The _user manager /// private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; private readonly IAuthorizationContext _authContext; @@ -99,17 +53,6 @@ namespace MediaBrowser.Api.Movies _authContext = authContext; } - public object Get(GetMovieRecommendations request) - { - var user = _userManager.GetUserById(request.UserId); - - var dtoOptions = GetDtoOptions(_authContext, request); - - var result = GetRecommendationCategories(user, request.ParentId, request.CategoryLimit, request.ItemLimit, dtoOptions); - - return ToOptimizedResult(result); - } - public QueryResult GetSimilarItemsResult(BaseGetSimilarItemsFromItem request) { var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; @@ -149,270 +92,5 @@ namespace MediaBrowser.Api.Movies return result; } - - private IEnumerable GetRecommendationCategories(User user, string parentId, int categoryLimit, int itemLimit, DtoOptions dtoOptions) - { - var categories = new List(); - - var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); - - var query = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] - { - typeof(Movie).Name, - //typeof(Trailer).Name, - //typeof(LiveTvProgram).Name - }, - // IsMovie = true - OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), - Limit = 7, - ParentId = parentIdGuid, - Recursive = true, - IsPlayed = true, - DtoOptions = dtoOptions - }; - - var recentlyPlayedMovies = _libraryManager.GetItemList(query); - - var itemTypes = new List { typeof(Movie).Name }; - if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions) - { - itemTypes.Add(typeof(Trailer).Name); - itemTypes.Add(typeof(LiveTvProgram).Name); - } - - var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), - Limit = 10, - IsFavoriteOrLiked = true, - ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), - EnableGroupByMetadataKey = true, - ParentId = parentIdGuid, - Recursive = true, - DtoOptions = dtoOptions - - }); - - var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList(); - // Get recently played directors - var recentDirectors = GetDirectors(mostRecentMovies) - .ToList(); - - // Get recently played actors - var recentActors = GetActors(mostRecentMovies) - .ToList(); - - var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator(); - var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator(); - - var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator(); - var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator(); - - var categoryTypes = new List> - { - // Give this extra weight - similarToRecentlyPlayed, - similarToRecentlyPlayed, - - // Give this extra weight - similarToLiked, - similarToLiked, - - hasDirectorFromRecentlyPlayed, - hasActorFromRecentlyPlayed - }; - - while (categories.Count < categoryLimit) - { - var allEmpty = true; - - foreach (var category in categoryTypes) - { - if (category.MoveNext()) - { - categories.Add(category.Current); - allEmpty = false; - - if (categories.Count >= categoryLimit) - { - break; - } - } - } - - if (allEmpty) - { - break; - } - } - - return categories.OrderBy(i => i.RecommendationType); - } - - private IEnumerable GetWithDirector( - User user, - IEnumerable names, - int itemLimit, - DtoOptions dtoOptions, - RecommendationType type) - { - var itemTypes = new List { typeof(Movie).Name }; - if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions) - { - itemTypes.Add(typeof(Trailer).Name); - itemTypes.Add(typeof(LiveTvProgram).Name); - } - - foreach (var name in names) - { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - Person = name, - // Account for duplicates by imdb id, since the database doesn't support this yet - Limit = itemLimit + 2, - PersonTypes = new[] { PersonType.Director }, - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - EnableGroupByMetadataKey = true, - DtoOptions = dtoOptions - - }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Select(x => x.First()) - .Take(itemLimit) - .ToList(); - - if (items.Count > 0) - { - var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); - - yield return new RecommendationDto - { - BaselineItemName = name, - CategoryId = name.GetMD5(), - RecommendationType = type, - Items = returnItems - }; - } - } - } - - private IEnumerable GetWithActor(User user, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) - { - var itemTypes = new List { typeof(Movie).Name }; - if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions) - { - itemTypes.Add(typeof(Trailer).Name); - itemTypes.Add(typeof(LiveTvProgram).Name); - } - - foreach (var name in names) - { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - Person = name, - // Account for duplicates by imdb id, since the database doesn't support this yet - Limit = itemLimit + 2, - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - EnableGroupByMetadataKey = true, - DtoOptions = dtoOptions - - }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Select(x => x.First()) - .Take(itemLimit) - .ToList(); - - if (items.Count > 0) - { - var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); - - yield return new RecommendationDto - { - BaselineItemName = name, - CategoryId = name.GetMD5(), - RecommendationType = type, - Items = returnItems - }; - } - } - } - - private IEnumerable GetSimilarTo(User user, List baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) - { - var itemTypes = new List { typeof(Movie).Name }; - if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions) - { - itemTypes.Add(typeof(Trailer).Name); - itemTypes.Add(typeof(LiveTvProgram).Name); - } - - foreach (var item in baselineItems) - { - var similar = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - Limit = itemLimit, - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - SimilarTo = item, - EnableGroupByMetadataKey = true, - DtoOptions = dtoOptions - - }); - - if (similar.Count > 0) - { - var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user); - - yield return new RecommendationDto - { - BaselineItemName = item.Name, - CategoryId = item.Id, - RecommendationType = type, - Items = returnItems - }; - } - } - } - - private IEnumerable GetActors(List items) - { - var people = _libraryManager.GetPeople(new InternalPeopleQuery - { - ExcludePersonTypes = new[] - { - PersonType.Director - }, - MaxListOrder = 3 - }); - - var itemIds = items.Select(i => i.Id).ToList(); - - return people - .Where(i => itemIds.Contains(i.ItemId)) - .Select(i => i.Name) - .DistinctNames(); - } - - private IEnumerable GetDirectors(List items) - { - var people = _libraryManager.GetPeople(new InternalPeopleQuery - { - PersonTypes = new[] - { - PersonType.Director - } - }); - - var itemIds = items.Select(i => i.Id).ToList(); - - return people - .Where(i => itemIds.Contains(i.ItemId)) - .Select(i => i.Name) - .DistinctNames(); - } } } From f17d198eb5643f551973b1e54405a472fe0b55b2 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 22:18:46 +0200 Subject: [PATCH 225/724] Move SuggestionsService to Jellyfin.Api --- .../Controllers/SuggestionsController.cs | 86 ++++++++++++++++ MediaBrowser.Api/SuggestionsService.cs | 98 ------------------- 2 files changed, 86 insertions(+), 98 deletions(-) create mode 100644 Jellyfin.Api/Controllers/SuggestionsController.cs delete mode 100644 MediaBrowser.Api/SuggestionsService.cs diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs new file mode 100644 index 0000000000..2d6445c305 --- /dev/null +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using Jellyfin.Api.Extensions; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The suggestions controller. + /// + public class SuggestionsController : BaseJellyfinApiController + { + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public SuggestionsController( + IDtoService dtoService, + IUserManager userManager, + ILibraryManager libraryManager) + { + _dtoService = dtoService; + _userManager = userManager; + _libraryManager = libraryManager; + } + + /// + /// Gets suggestions. + /// + /// The user id. + /// The media types. + /// The type. + /// Whether to enable the total record count. + /// Optional. The start index. + /// Optional. The limit. + /// Suggestions returned. + /// A with the suggestions. + [HttpGet("/Users/{userId}/Suggestions")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSuggestions( + [FromRoute] Guid userId, + [FromQuery] string? mediaType, + [FromQuery] string? type, + [FromQuery] bool enableTotalRecordCount, + [FromQuery] int? startIndex, + [FromQuery] int? limit) + { + var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + + var dtoOptions = new DtoOptions().AddClientFields(Request); + var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) + { + OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), + MediaTypes = (mediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + IncludeItemTypes = (type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + IsVirtualItem = false, + StartIndex = startIndex, + Limit = limit, + DtoOptions = dtoOptions, + EnableTotalRecordCount = enableTotalRecordCount, + Recursive = true + }); + + var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); + + return new QueryResult + { + TotalRecordCount = result.TotalRecordCount, + Items = dtoList + }; + } + } +} diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs deleted file mode 100644 index 32d3bde5cb..0000000000 --- a/MediaBrowser.Api/SuggestionsService.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Linq; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Users/{UserId}/Suggestions", "GET", Summary = "Gets items based on a query.")] - public class GetSuggestedItems : IReturn> - { - public string MediaType { get; set; } - public string Type { get; set; } - public Guid UserId { get; set; } - public bool EnableTotalRecordCount { get; set; } - public int? StartIndex { get; set; } - public int? Limit { get; set; } - - public string[] GetMediaTypes() - { - return (MediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public string[] GetIncludeItemTypes() - { - return (Type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - } - - public class SuggestionsService : BaseApiService - { - private readonly IDtoService _dtoService; - private readonly IAuthorizationContext _authContext; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - - public SuggestionsService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IDtoService dtoService, - IAuthorizationContext authContext, - IUserManager userManager, - ILibraryManager libraryManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _dtoService = dtoService; - _authContext = authContext; - _userManager = userManager; - _libraryManager = libraryManager; - } - - public object Get(GetSuggestedItems request) - { - return GetResultItems(request); - } - - private QueryResult GetResultItems(GetSuggestedItems request) - { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var dtoOptions = GetDtoOptions(_authContext, request); - var result = GetItems(request, user, dtoOptions); - - var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); - - return new QueryResult - { - TotalRecordCount = result.TotalRecordCount, - Items = dtoList - }; - } - - private QueryResult GetItems(GetSuggestedItems request, User user, DtoOptions dtoOptions) - { - return _libraryManager.GetItemsResult(new InternalItemsQuery(user) - { - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), - MediaTypes = request.GetMediaTypes(), - IncludeItemTypes = request.GetIncludeItemTypes(), - IsVirtualItem = false, - StartIndex = request.StartIndex, - Limit = request.Limit, - DtoOptions = dtoOptions, - EnableTotalRecordCount = request.EnableTotalRecordCount, - Recursive = true - }); - } - } -} From cd273c4e98246420edf39ef63c906fbc3725d8e4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 15:08:35 -0600 Subject: [PATCH 226/724] Start move ImageService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/ImageController.cs | 139 ++++++++++++++++++++ MediaBrowser.Api/Images/ImageService.cs | 77 ----------- 2 files changed, 139 insertions(+), 77 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ImageController.cs diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs new file mode 100644 index 0000000000..6742bffc61 --- /dev/null +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Image controller. + /// + public class ImageController : BaseJellyfinApiController + { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly IImageProcessor _imageProcessor; + private readonly IFileSystem _fileSystem; + private readonly IAuthorizationContext _authContext; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ImageController( + IUserManager userManager, + ILibraryManager libraryManager, + IProviderManager providerManager, + IImageProcessor imageProcessor, + IFileSystem fileSystem, + IAuthorizationContext authContext, + ILogger logger, + IServerConfigurationManager serverConfigurationManager) + { + _userManager = userManager; + _libraryManager = libraryManager; + _providerManager = providerManager; + _imageProcessor = imageProcessor; + _fileSystem = fileSystem; + _authContext = authContext; + _logger = logger; + _serverConfigurationManager = serverConfigurationManager; + } + + /// + /// Sets the user image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image updated. + /// A . + [HttpPost("/Users/{userId}/Images/{imageType}")] + [HttpPost("/Users/{userId}/Images/{imageType}/{index}")] + public async Task PostUserImage( + [FromRoute] Guid userId, + [FromRoute] ImageType imageType, + [FromRoute] int? index) + { + // TODO AssertCanUpdateUser(_authContext, _userManager, id, true); + + var user = _userManager.GetUserById(userId); + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); + + await _providerManager + .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Delete the user's image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image deleted. + /// A . + [HttpDelete("/Users/{userId}/Images/{itemType}")] + [HttpDelete("/Users/{userId}/Images/{itemType}/{index}")] + public ActionResult DeleteUserImage( + [FromRoute] Guid userId, + [FromRoute] ImageType imageType, + [FromRoute] int? index) + { + // TODO AssertCanUpdateUser(_authContext, _userManager, userId, true); + + var user = _userManager.GetUserById(userId); + try + { + System.IO.File.Delete(user.ProfileImage.Path); + } + catch (IOException e) + { + _logger.LogError(e, "Error deleting user profile image:"); + } + + _userManager.ClearProfileImage(user); + return NoContent(); + } + + private static async Task GetMemoryStream(Stream inputStream) + { + using var reader = new StreamReader(inputStream); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); + + var bytes = Convert.FromBase64String(text); + return new MemoryStream(bytes) + { + Position = 0 + }; + } + } +} diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 0b8ddeacdf..48c879bb72 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -163,44 +163,6 @@ namespace MediaBrowser.Api.Images public string Id { get; set; } } - /// - /// Class DeleteUserImage - /// - [Route("/Users/{Id}/Images/{Type}", "DELETE")] - [Route("/Users/{Id}/Images/{Type}/{Index}", "DELETE")] - [Authenticated] - public class DeleteUserImage : DeleteImageRequest, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - } - - /// - /// Class PostUserImage - /// - [Route("/Users/{Id}/Images/{Type}", "POST")] - [Route("/Users/{Id}/Images/{Type}/{Index}", "POST")] - [Authenticated] - public class PostUserImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// The raw Http Request Input Stream - /// - /// The request stream. - public Stream RequestStream { get; set; } - } - /// /// Class PostItemImage /// @@ -438,23 +400,6 @@ namespace MediaBrowser.Api.Images return GetImage(request, item.Id, item, true); } - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(PostUserImage request) - { - var id = Guid.Parse(GetPathValue(1)); - - AssertCanUpdateUser(_authContext, _userManager, id, true); - - request.Type = Enum.Parse(GetPathValue(3).ToString(), true); - - var user = _userManager.GetUserById(id); - - return PostImage(user, request.RequestStream, Request.ContentType); - } - /// /// Posts the specified request. /// @@ -470,28 +415,6 @@ namespace MediaBrowser.Api.Images return PostImage(item, request.RequestStream, request.Type, Request.ContentType); } - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(DeleteUserImage request) - { - var userId = request.Id; - AssertCanUpdateUser(_authContext, _userManager, userId, true); - - var user = _userManager.GetUserById(userId); - try - { - File.Delete(user.ProfileImage.Path); - } - catch (IOException e) - { - Logger.LogError(e, "Error deleting user profile image:"); - } - - _userManager.ClearProfileImage(user); - } - /// /// Deletes the specified request. /// From 33de0ac10880a941a10f28c64f18253cc711b8da Mon Sep 17 00:00:00 2001 From: David Date: Sat, 20 Jun 2020 12:10:45 +0200 Subject: [PATCH 227/724] Use RequestHelpers.Split --- Jellyfin.Api/Controllers/SuggestionsController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 2d6445c305..e1a99a1385 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -64,8 +65,8 @@ namespace Jellyfin.Api.Controllers var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), - MediaTypes = (mediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - IncludeItemTypes = (type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + MediaTypes = RequestHelpers.Split(mediaType!, ',', true), + IncludeItemTypes = RequestHelpers.Split(type!, ',', true), IsVirtualItem = false, StartIndex = startIndex, Limit = limit, From 64fb173dad77a38273548434bee683b85e323345 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 20 Jun 2020 15:59:41 +0200 Subject: [PATCH 228/724] Move DashboardController to Jellyfin.Api --- .../ApplicationHost.cs | 4 - .../Emby.Server.Implementations.csproj | 1 - .../Controllers/DashboardController.cs | 264 ++++++++++++++ .../Models}/ConfigurationPageInfo.cs | 38 +- Jellyfin.Server/Program.cs | 4 +- .../Api/DashboardService.cs | 340 ------------------ .../MediaBrowser.WebDashboard.csproj | 42 --- .../Properties/AssemblyInfo.cs | 21 -- MediaBrowser.WebDashboard/ServerEntryPoint.cs | 42 --- MediaBrowser.sln | 6 - 10 files changed, 296 insertions(+), 466 deletions(-) create mode 100644 Jellyfin.Api/Controllers/DashboardController.cs rename {MediaBrowser.WebDashboard/Api => Jellyfin.Api/Models}/ConfigurationPageInfo.cs (55%) delete mode 100644 MediaBrowser.WebDashboard/Api/DashboardService.cs delete mode 100644 MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj delete mode 100644 MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs delete mode 100644 MediaBrowser.WebDashboard/ServerEntryPoint.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5772dd479d..25ee7e9ec0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -97,7 +97,6 @@ using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; -using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -1037,9 +1036,6 @@ namespace Emby.Server.Implementations // Include composable parts in the Api assembly yield return typeof(ApiEntryPoint).Assembly; - // Include composable parts in the Dashboard assembly - yield return typeof(DashboardService).Assembly; - // Include composable parts in the Model assembly yield return typeof(SystemInfo).Assembly; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index e71e437acd..5272e2692f 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -13,7 +13,6 @@ - diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs new file mode 100644 index 0000000000..6a7bf7d0aa --- /dev/null +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Jellyfin.Api.Models; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Plugins; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The dashboard controller. + /// + public class DashboardController : BaseJellyfinApiController + { + private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _appConfig; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IResourceFileManager _resourceFileManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public DashboardController( + IServerApplicationHost appHost, + IConfiguration appConfig, + IResourceFileManager resourceFileManager, + IServerConfigurationManager serverConfigurationManager) + { + _appHost = appHost; + _appConfig = appConfig; + _resourceFileManager = resourceFileManager; + _serverConfigurationManager = serverConfigurationManager; + } + + /// + /// Gets the path of the directory containing the static web interface content, or null if the server is not + /// hosting the web client. + /// + private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager); + + /// + /// Gets the configuration pages. + /// + /// Whether to enable in the main menu. + /// The . + /// ConfigurationPages returned. + /// Server still loading. + /// An with infos about the plugins. + [HttpGet("/web/ConfigurationPages")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetConfigurationPages( + [FromQuery] bool? enableInMainMenu, + [FromQuery] ConfigurationPageType? pageType) + { + const string unavailableMessage = "The server is still loading. Please try again momentarily."; + + var pages = _appHost.GetExports().ToList(); + + if (pages == null) + { + return NotFound(unavailableMessage); + } + + // Don't allow a failing plugin to fail them all + var configPages = pages.Select(p => + { + return new ConfigurationPageInfo(p); + }) + .Where(i => i != null) + .ToList(); + + configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); + + if (pageType != null) + { + configPages = configPages.Where(p => p.ConfigurationPageType == pageType).ToList(); + } + + if (enableInMainMenu.HasValue) + { + configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList(); + } + + return configPages; + } + + /// + /// Gets a dashboard configuration page. + /// + /// The name of the page. + /// ConfigurationPage returned. + /// Plugin configuration page not found. + /// The configuration page. + [HttpGet("/web/ConfigurationPage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetDashboardConfigurationPage([FromQuery] string name) + { + IPlugin? plugin = null; + Stream? stream = null; + + var isJs = false; + var isTemplate = false; + + var page = _appHost.GetExports().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (page != null) + { + plugin = page.Plugin; + stream = page.GetHtmlStream(); + } + + if (plugin == null) + { + var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase)); + if (altPage != null) + { + plugin = altPage.Item2; + stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); + + isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); + isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); + } + } + + if (plugin != null && stream != null) + { + if (isJs) + { + return File(stream, MimeTypes.GetMimeType("page.js")); + } + + if (isTemplate) + { + return File(stream, MimeTypes.GetMimeType("page.html")); + } + + return File(stream, MimeTypes.GetMimeType("page.html")); + } + + return NotFound(); + } + + /// + /// Gets the robots.txt. + /// + /// Robots.txt returned. + /// The robots.txt. + [HttpGet("/robots.txt")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetRobotsTxt() + { + return GetWebClientResource("robots.txt", string.Empty); + } + + /// + /// Gets a resource from the web client. + /// + /// The resource name. + /// The v. + /// Web client returned. + /// Server does not host a web client. + /// The resource. + [HttpGet("/web/{*resourceName}")] + [ApiExplorerSettings(IgnoreApi = true)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")] + public ActionResult GetWebClientResource( + [FromRoute] string resourceName, + [FromQuery] string? v) + { + if (!_appConfig.HostWebClient() || WebClientUiPath == null) + { + return NotFound("Server does not host a web client."); + } + + var path = resourceName; + var basePath = WebClientUiPath; + + // Bounce them to the startup wizard if it hasn't been completed yet + if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted + && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase) + && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase)) + { + return Redirect("index.html?start=wizard#!/wizardstart.html"); + } + + var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read); + return File(stream, MimeTypes.GetMimeType(path)); + } + + /// + /// Gets the favicon. + /// + /// Favicon.ico returned. + /// The favicon. + [HttpGet("/favicon.ico")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetFavIcon() + { + return GetWebClientResource("favicon.ico", string.Empty); + } + + /// + /// Gets the path of the directory containing the static web interface content. + /// + /// The app configuration. + /// The server configuration manager. + /// The directory path, or null if the server is not hosting the web client. + public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) + { + if (!appConfig.HostWebClient()) + { + return null; + } + + if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) + { + return serverConfigManager.Configuration.DashboardSourcePath; + } + + return serverConfigManager.ApplicationPaths.WebPath; + } + + private IEnumerable GetConfigPages(IPlugin plugin) + { + return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); + } + + private IEnumerable> GetPluginPages(IPlugin plugin) + { + var hasConfig = plugin as IHasWebPages; + + if (hasConfig == null) + { + return new List>(); + } + + return hasConfig.GetPages().Select(i => new Tuple(i, plugin)); + } + + private IEnumerable> GetPluginPages() + { + return _appHost.Plugins.SelectMany(GetPluginPages); + } + } +} diff --git a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs similarity index 55% rename from MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs rename to Jellyfin.Api/Models/ConfigurationPageInfo.cs index e49a4be8af..2aa6373aa9 100644 --- a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs +++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs @@ -1,13 +1,18 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; -namespace MediaBrowser.WebDashboard.Api +namespace Jellyfin.Api.Models { + /// + /// The configuration page info. + /// public class ConfigurationPageInfo { + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. public ConfigurationPageInfo(IPluginConfigurationPage page) { Name = page.Name; @@ -22,6 +27,11 @@ namespace MediaBrowser.WebDashboard.Api } } + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) { Name = page.Name; @@ -40,13 +50,25 @@ namespace MediaBrowser.WebDashboard.Api /// The name. public string Name { get; set; } + /// + /// Gets or sets a value indicating whether the configurations page is enabled in the main menu. + /// public bool EnableInMainMenu { get; set; } - public string MenuSection { get; set; } + /// + /// Gets or sets the menu section. + /// + public string? MenuSection { get; set; } - public string MenuIcon { get; set; } + /// + /// Gets or sets the menu icon. + /// + public string? MenuIcon { get; set; } - public string DisplayName { get; set; } + /// + /// Gets or sets the display name. + /// + public string? DisplayName { get; set; } /// /// Gets or sets the type of the configuration page. @@ -58,6 +80,6 @@ namespace MediaBrowser.WebDashboard.Api /// Gets or sets the plugin id. /// /// The plugin id. - public string PluginId { get; set; } + public string? PluginId { get; set; } } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3971a08e91..dfc7bbbb10 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -14,9 +14,9 @@ using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; +using Jellyfin.Api.Controllers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; -using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; @@ -172,7 +172,7 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager); + string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager); if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) { throw new InvalidOperationException( diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs deleted file mode 100644 index 63cbfd9e42..0000000000 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ /dev/null @@ -1,340 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1402 -#pragma warning disable SA1649 - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.WebDashboard.Api -{ - /// - /// Class GetDashboardConfigurationPages. - /// - [Route("/web/ConfigurationPages", "GET")] - public class GetDashboardConfigurationPages : IReturn> - { - /// - /// Gets or sets the type of the page. - /// - /// The type of the page. - public ConfigurationPageType? PageType { get; set; } - - public bool? EnableInMainMenu { get; set; } - } - - /// - /// Class GetDashboardConfigurationPage. - /// - [Route("/web/ConfigurationPage", "GET")] - public class GetDashboardConfigurationPage - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - } - - [Route("/robots.txt", "GET", IsHidden = true)] - public class GetRobotsTxt - { - } - - /// - /// Class GetDashboardResource. - /// - [Route("/web/{ResourceName*}", "GET", IsHidden = true)] - public class GetDashboardResource - { - /// - /// Gets or sets the name. - /// - /// The name. - public string ResourceName { get; set; } - - /// - /// Gets or sets the V. - /// - /// The V. - public string V { get; set; } - } - - [Route("/favicon.ico", "GET", IsHidden = true)] - public class GetFavIcon - { - } - - /// - /// Class DashboardService. - /// - public class DashboardService : IService, IRequiresRequest - { - /// - /// Gets or sets the logger. - /// - /// The logger. - private readonly ILogger _logger; - - /// - /// Gets or sets the HTTP result factory. - /// - /// The HTTP result factory. - private readonly IHttpResultFactory _resultFactory; - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IFileSystem _fileSystem; - private readonly IResourceFileManager _resourceFileManager; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The application host. - /// The application configuration. - /// The resource file manager. - /// The server configuration manager. - /// The file system. - /// The result factory. - public DashboardService( - ILogger logger, - IServerApplicationHost appHost, - IConfiguration appConfig, - IResourceFileManager resourceFileManager, - IServerConfigurationManager serverConfigurationManager, - IFileSystem fileSystem, - IHttpResultFactory resultFactory) - { - _logger = logger; - _appHost = appHost; - _appConfig = appConfig; - _resourceFileManager = resourceFileManager; - _serverConfigurationManager = serverConfigurationManager; - _fileSystem = fileSystem; - _resultFactory = resultFactory; - } - - /// - /// Gets or sets the request context. - /// - /// The request context. - public IRequest Request { get; set; } - - /// - /// Gets the path of the directory containing the static web interface content, or null if the server is not - /// hosting the web client. - /// - public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager); - - /// - /// Gets the path of the directory containing the static web interface content. - /// - /// The app configuration. - /// The server configuration manager. - /// The directory path, or null if the server is not hosting the web client. - public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) - { - if (!appConfig.HostWebClient()) - { - return null; - } - - if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) - { - return serverConfigManager.Configuration.DashboardSourcePath; - } - - return serverConfigManager.ApplicationPaths.WebPath; - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetFavIcon request) - { - return Get(new GetDashboardResource - { - ResourceName = "favicon.ico" - }); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public Task Get(GetDashboardConfigurationPage request) - { - IPlugin plugin = null; - Stream stream = null; - - var isJs = false; - var isTemplate = false; - - var page = ServerEntryPoint.Instance.PluginConfigurationPages.FirstOrDefault(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase)); - if (page != null) - { - plugin = page.Plugin; - stream = page.GetHtmlStream(); - } - - if (plugin == null) - { - var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, request.Name, StringComparison.OrdinalIgnoreCase)); - if (altPage != null) - { - plugin = altPage.Item2; - stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); - - isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); - isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); - } - } - - if (plugin != null && stream != null) - { - if (isJs) - { - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.js"), () => Task.FromResult(stream)); - } - - if (isTemplate) - { - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); - } - - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); - } - - throw new ResourceNotFoundException(); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetDashboardConfigurationPages request) - { - const string unavailableMessage = "The server is still loading. Please try again momentarily."; - - var instance = ServerEntryPoint.Instance; - - if (instance == null) - { - throw new InvalidOperationException(unavailableMessage); - } - - var pages = instance.PluginConfigurationPages; - - if (pages == null) - { - throw new InvalidOperationException(unavailableMessage); - } - - // Don't allow a failing plugin to fail them all - var configPages = pages.Select(p => - { - try - { - return new ConfigurationPageInfo(p); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name); - return null; - } - }) - .Where(i => i != null) - .ToList(); - - configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); - - if (request.PageType.HasValue) - { - configPages = configPages.Where(p => p.ConfigurationPageType == request.PageType.Value).ToList(); - } - - if (request.EnableInMainMenu.HasValue) - { - configPages = configPages.Where(p => p.EnableInMainMenu == request.EnableInMainMenu.Value).ToList(); - } - - return configPages; - } - - private IEnumerable> GetPluginPages() - { - return _appHost.Plugins.SelectMany(GetPluginPages); - } - - private IEnumerable> GetPluginPages(IPlugin plugin) - { - var hasConfig = plugin as IHasWebPages; - - if (hasConfig == null) - { - return new List>(); - } - - return hasConfig.GetPages().Select(i => new Tuple(i, plugin)); - } - - private IEnumerable GetConfigPages(IPlugin plugin) - { - return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetRobotsTxt request) - { - return Get(new GetDashboardResource - { - ResourceName = "robots.txt" - }); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task Get(GetDashboardResource request) - { - if (!_appConfig.HostWebClient() || DashboardUIPath == null) - { - throw new ResourceNotFoundException(); - } - - var path = request?.ResourceName; - var basePath = DashboardUIPath; - - // Bounce them to the startup wizard if it hasn't been completed yet - if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted - && !Request.RawUrl.Contains("wizard", StringComparison.OrdinalIgnoreCase) - && Request.RawUrl.Contains("index", StringComparison.OrdinalIgnoreCase)) - { - Request.Response.Redirect("index.html?start=wizard#!/wizardstart.html"); - return null; - } - - return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)).ConfigureAwait(false); - } - } -} diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj deleted file mode 100644 index bcaee50f29..0000000000 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - - {5624B7B5-B5A7-41D8-9F10-CC5611109619} - - - - - - - - - - - - - - PreserveNewest - - - - - netstandard2.1 - false - true - true - - - - - - - - - - - - ../jellyfin.ruleset - - - diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs deleted file mode 100644 index 584d490216..0000000000 --- a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MediaBrowser.WebDashboard")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/MediaBrowser.WebDashboard/ServerEntryPoint.cs b/MediaBrowser.WebDashboard/ServerEntryPoint.cs deleted file mode 100644 index 5c7e8b3c76..0000000000 --- a/MediaBrowser.WebDashboard/ServerEntryPoint.cs +++ /dev/null @@ -1,42 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Controller.Plugins; - -namespace MediaBrowser.WebDashboard -{ - public sealed class ServerEntryPoint : IServerEntryPoint - { - private readonly IApplicationHost _appHost; - - public ServerEntryPoint(IApplicationHost appHost) - { - _appHost = appHost; - Instance = this; - } - - public static ServerEntryPoint Instance { get; private set; } - - /// - /// Gets the list of plugin configuration pages. - /// - /// The configuration pages. - public List PluginConfigurationPages { get; private set; } - - /// - public Task RunAsync() - { - PluginConfigurationPages = _appHost.GetExports().ToList(); - - return Task.CompletedTask; - } - - /// - public void Dispose() - { - } - } -} diff --git a/MediaBrowser.sln b/MediaBrowser.sln index e100c0b1cd..0362eff1c8 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "Medi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" @@ -94,10 +92,6 @@ Global {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU From 0c98bc42a8726dfa09244d55acf5339b3bd7a403 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 15:11:10 -0600 Subject: [PATCH 229/724] Fix response code & docs --- .../Controllers/ScheduledTasksController.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index f7122c4134..64de23ef2a 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -88,9 +88,9 @@ namespace Jellyfin.Api.Controllers /// Start specified task. /// /// Task Id. - /// Task started. + /// Task started. /// Task not found. - /// An on success, or a if the file could not be found. + /// An on success, or a if the file could not be found. [HttpPost("Running/{taskId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -105,14 +105,14 @@ namespace Jellyfin.Api.Controllers } _taskManager.Execute(task, new TaskOptions()); - return Ok(); + return NoContent(); } /// /// Stop specified task. /// /// Task Id. - /// Task stopped. + /// Task stopped. /// Task not found. /// An on success, or a if the file could not be found. [HttpDelete("Running/{taskId}")] @@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers } _taskManager.Cancel(task); - return Ok(); + return NoContent(); } /// @@ -137,7 +137,7 @@ namespace Jellyfin.Api.Controllers /// /// Task Id. /// Triggers. - /// Task triggers updated. + /// Task triggers updated. /// Task not found. /// An on success, or a if the file could not be found. [HttpPost("{taskId}/Triggers")] @@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers } task.Triggers = triggerInfos; - return Ok(); + return NoContent(); } } } From 95bae56640289cf11886b0036fb6a685353e3dc4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 15:14:43 -0600 Subject: [PATCH 230/724] Fix response code & docs --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 2537996512..0c66ff875f 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -60,9 +60,9 @@ namespace Jellyfin.Api.Controllers /// /// The item id. /// The new item properties. - /// Item updated. + /// Item updated. /// Item not found. - /// An on success, or a if the item could not be found. + /// An on success, or a if the item could not be found. [HttpPost("/Items/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers RefreshPriority.High); } - return Ok(); + return NoContent(); } /// @@ -187,9 +187,9 @@ namespace Jellyfin.Api.Controllers /// /// The item id. /// The content type of the item. - /// Item content type updated. + /// Item content type updated. /// Item not found. - /// An on success, or a if the item could not be found. + /// An on success, or a if the item could not be found. [HttpPost("/Items/{itemId}/ContentType")] public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType) { @@ -217,7 +217,7 @@ namespace Jellyfin.Api.Controllers _serverConfigurationManager.Configuration.ContentTypes = types.ToArray(); _serverConfigurationManager.SaveConfiguration(); - return Ok(); + return NoContent(); } private void UpdateItem(BaseItemDto request, BaseItem item) From 8c38644fcadda10dd6132c3f43b333ccdeb7b392 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 15:15:59 -0600 Subject: [PATCH 231/724] Fix response code & docs --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 0c66ff875f..384f250ecc 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// An on success, or a if the item could not be found. [HttpPost("/Items/{itemId}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request) { @@ -191,6 +191,8 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// An on success, or a if the item could not be found. [HttpPost("/Items/{itemId}/ContentType")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType) { var item = _libraryManager.GetItemById(itemId); From 81c0451b5e578bb8a41dcb81f2766dbd1eb7f055 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 15:16:30 -0600 Subject: [PATCH 232/724] Fix response code & docs --- Jellyfin.Api/Controllers/ScheduledTasksController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 64de23ef2a..bf5c3076e0 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// Task not found. /// An on success, or a if the file could not be found. [HttpPost("Running/{taskId}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult StartTask([FromRoute] string taskId) { @@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers /// Task not found. /// An on success, or a if the file could not be found. [HttpDelete("Running/{taskId}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult StopTask([FromRoute] string taskId) { @@ -141,7 +141,7 @@ namespace Jellyfin.Api.Controllers /// Task not found. /// An on success, or a if the file could not be found. [HttpPost("{taskId}/Triggers")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateTask( [FromRoute] string taskId, From d1ca0cb4c7161b420c32e48824cc5065054b1869 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 16:03:19 -0600 Subject: [PATCH 233/724] Use proper DtoOptions extensions --- .../Controllers/PlaylistsController.cs | 33 ++++++++++--------- Jellyfin.Api/Extensions/DtoExtensions.cs | 2 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 18 ++++++++++ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 0d73962de4..9e2a91e102 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaylistDtos; using MediaBrowser.Controller.Dto; @@ -80,17 +81,17 @@ namespace Jellyfin.Api.Controllers /// The playlist id. /// Item id, comma delimited. /// The userId. - /// Items added to playlist. - /// An on success. + /// Items added to playlist. + /// An on success. [HttpPost("{playlistId}/Items")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddToPlaylist( [FromRoute] string playlistId, [FromQuery] string ids, [FromQuery] Guid userId) { _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId); - return Ok(); + return NoContent(); } /// @@ -99,17 +100,17 @@ namespace Jellyfin.Api.Controllers /// The playlist id. /// The item id. /// The new index. - /// Item moved to new index. - /// An on success. + /// Item moved to new index. + /// An on success. [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult MoveItem( [FromRoute] string playlistId, [FromRoute] string itemId, [FromRoute] int newIndex) { _playlistManager.MoveItem(playlistId, itemId, newIndex); - return Ok(); + return NoContent(); } /// @@ -117,14 +118,14 @@ namespace Jellyfin.Api.Controllers /// /// The playlist id. /// The item ids, comma delimited. - /// Items removed. - /// An on success. + /// Items removed. + /// An on success. [HttpDelete("{playlistId}/Items")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds) { _playlistManager.RemoveFromPlaylist(playlistId, entryIds.Split(',')); - return Ok(); + return NoContent(); } /// @@ -151,7 +152,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] string fields, [FromRoute] bool? enableImages, [FromRoute] bool? enableUserData, - [FromRoute] bool? imageTypeLimit, + [FromRoute] int? imageTypeLimit, [FromRoute] string enableImageTypes) { var playlist = (Playlist)_libraryManager.GetItemById(playlistId); @@ -176,8 +177,10 @@ namespace Jellyfin.Api.Controllers items = items.Take(limit.Value).ToArray(); } - // TODO var dtoOptions = GetDtoOptions(_authContext, request); - var dtoOptions = new DtoOptions(); + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index 4c587391fc..ac248109d7 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -122,7 +122,7 @@ namespace Jellyfin.Api.Extensions /// Enable image types. /// Modified DtoOptions object. internal static DtoOptions AddAdditionalDtoOptions( - in DtoOptions dtoOptions, + this DtoOptions dtoOptions, bool? enableImages, bool? enableUserData, int? imageTypeLimit, diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 2ff40a8a5e..e2a0cf4faf 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; @@ -73,5 +74,22 @@ namespace Jellyfin.Api.Helpers return session; } + + /// + /// Get Guid array from string. + /// + /// String value. + /// Guid array. + internal static Guid[] GetGuids(string? value) + { + if (value == null) + { + return Array.Empty(); + } + + return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(i => new Guid(i)) + .ToArray(); + } } } From 472fd5217f25b6849ee4c1de7da92c70b5c1a9b1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 16:07:09 -0600 Subject: [PATCH 234/724] clean up --- Jellyfin.Api/Controllers/PlaylistsController.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 9e2a91e102..2e3f6c54af 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -1,7 +1,4 @@ -#nullable enable -#pragma warning disable CA1801 - -using System; +using System; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Extensions; @@ -124,7 +121,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds) { - _playlistManager.RemoveFromPlaylist(playlistId, entryIds.Split(',')); + _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true)); return NoContent(); } From f017f5c97fb091304bba819e9ba73510cf85a9b1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 16:07:53 -0600 Subject: [PATCH 235/724] clean up --- Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 20835eecbd..0d67c86f71 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -1,5 +1,4 @@ -#nullable enable -using System; +using System; namespace Jellyfin.Api.Models.PlaylistDtos { From 9a8deadc215aa1ca25e1667c8c373a13e07d301e Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 17:06:33 -0600 Subject: [PATCH 236/724] implement all non image get endpoints --- Jellyfin.Api/Controllers/ImageController.cs | 242 +++++++++++++++++- MediaBrowser.Api/Images/ImageService.cs | 261 -------------------- 2 files changed, 236 insertions(+), 267 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 6742bffc61..d8c67dbea0 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1,15 +1,24 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -69,13 +78,18 @@ namespace Jellyfin.Api.Controllers /// Image updated. /// A . [HttpPost("/Users/{userId}/Images/{imageType}")] - [HttpPost("/Users/{userId}/Images/{imageType}/{index}")] + [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task PostUserImage( [FromRoute] Guid userId, [FromRoute] ImageType imageType, - [FromRoute] int? index) + [FromRoute] int? index = null) { - // TODO AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to update the image."); + } var user = _userManager.GetUserById(userId); await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); @@ -102,13 +116,19 @@ namespace Jellyfin.Api.Controllers /// Image deleted. /// A . [HttpDelete("/Users/{userId}/Images/{itemType}")] - [HttpDelete("/Users/{userId}/Images/{itemType}/{index}")] + [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteUserImage( [FromRoute] Guid userId, [FromRoute] ImageType imageType, - [FromRoute] int? index) + [FromRoute] int? index = null) { - // TODO AssertCanUpdateUser(_authContext, _userManager, userId, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to delete the image."); + } var user = _userManager.GetUserById(userId); try @@ -124,6 +144,164 @@ namespace Jellyfin.Api.Controllers return NoContent(); } + /// + /// Delete an item's image. + /// + /// Item id. + /// Image type. + /// The image index. + /// Image deleted. + /// Item not found. + /// A on success, or a if item not found. + [HttpDelete("/Items/{itemId}/Images/{imageType}")] + [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteItemImage( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + item.DeleteImage(imageType, imageIndex ?? 0); + return NoContent(); + } + + /// + /// Set item image. + /// + /// Item id. + /// Image type. + /// (Unused) Image index. + /// Image saved. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("/Items/{itemId}/Images/{imageType}")] + [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task SetItemImage( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + + return NoContent(); + } + + /// + /// Updates the index for an item image. + /// + /// Item id. + /// Image type. + /// Old image index. + /// New image index. + /// Image index updated. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex}/Index")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UpdateItemImageIndex( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int imageIndex, + [FromQuery] int newIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + item.SwapImages(imageType, imageIndex, newIndex); + return NoContent(); + } + + /// + /// Get item image infos. + /// + /// Item id. + /// Item images returned. + /// Item not found. + /// The list of image infos on success, or if item not found. + [HttpGet("/Items/{itemId}/Images")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetItemImageInfos([FromRoute] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + var list = new List(); + var itemImages = item.ImageInfos; + + if (itemImages.Length == 0) + { + // short-circuit + return list; + } + + _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct + + foreach (var image in itemImages) + { + if (!item.AllowsMultipleImages(image.Type)) + { + var info = GetImageInfo(item, image, null); + + if (info != null) + { + list.Add(info); + } + } + } + + foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) + { + var index = 0; + + // Prevent implicitly captured closure + var currentImageType = imageType; + + foreach (var image in itemImages.Where(i => i.Type == currentImageType)) + { + var info = GetImageInfo(item, image, index); + + if (info != null) + { + list.Add(info); + } + + index++; + } + } + + return list; + } + private static async Task GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); @@ -135,5 +313,57 @@ namespace Jellyfin.Api.Controllers Position = 0 }; } + + private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) + { + int? width = null; + int? height = null; + string? blurhash = null; + long length = 0; + + try + { + if (info.IsLocalFile) + { + var fileInfo = _fileSystem.GetFileInfo(info.Path); + length = fileInfo.Length; + + blurhash = info.BlurHash; + width = info.Width; + height = info.Height; + + if (width <= 0 || height <= 0) + { + width = null; + height = null; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting image information for {Item}", item.Name); + } + + try + { + return new ImageInfo + { + Path = info.Path, + ImageIndex = imageIndex, + ImageType = info.Type, + ImageTag = _imageProcessor.GetImageCacheTag(item, info), + Size = length, + BlurHash = blurhash, + Width = width, + Height = height + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting image information for {Path}", info.Path); + + return null; + } + } } } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 48c879bb72..a98266e0d2 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -15,7 +14,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; @@ -26,21 +24,6 @@ using User = Jellyfin.Data.Entities.User; namespace MediaBrowser.Api.Images { - /// - /// Class GetItemImage. - /// - [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")] - [Authenticated] - public class GetItemImageInfos : IReturn> - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - [Route("/Items/{Id}/Images/{Type}", "GET")] [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")] [Route("/Items/{Id}/Images/{Type}", "HEAD")] @@ -57,42 +40,6 @@ namespace MediaBrowser.Api.Images public Guid Id { get; set; } } - /// - /// Class UpdateItemImageIndex - /// - [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST", Summary = "Updates the index for an item image")] - [Authenticated(Roles = "admin")] - public class UpdateItemImageIndex : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the type of the image. - /// - /// The type of the image. - [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public ImageType Type { get; set; } - - /// - /// Gets or sets the index. - /// - /// The index. - [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] - public int Index { get; set; } - - /// - /// Gets or sets the new index. - /// - /// The new index. - [ApiMember(Name = "NewIndex", Description = "The new image index", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public int NewIndex { get; set; } - } - /// /// Class GetPersonImage /// @@ -147,44 +94,6 @@ namespace MediaBrowser.Api.Images public Guid Id { get; set; } } - /// - /// Class DeleteItemImage - /// - [Route("/Items/{Id}/Images/{Type}", "DELETE")] - [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")] - [Authenticated(Roles = "admin")] - public class DeleteItemImage : DeleteImageRequest, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - /// - /// Class PostItemImage - /// - [Route("/Items/{Id}/Images/{Type}", "POST")] - [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")] - [Authenticated(Roles = "admin")] - public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// The raw Http Request Input Stream - /// - /// The request stream. - public Stream RequestStream { get; set; } - } - /// /// Class ImageService /// @@ -223,126 +132,6 @@ namespace MediaBrowser.Api.Images _authContext = authContext; } - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetItemImageInfos request) - { - var item = _libraryManager.GetItemById(request.Id); - - var result = GetItemImageInfos(item); - - return ToOptimizedResult(result); - } - - /// - /// Gets the item image infos. - /// - /// The item. - /// Task{List{ImageInfo}}. - public List GetItemImageInfos(BaseItem item) - { - var list = new List(); - var itemImages = item.ImageInfos; - - if (itemImages.Length == 0) - { - // short-circuit - return list; - } - - _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct - - foreach (var image in itemImages) - { - if (!item.AllowsMultipleImages(image.Type)) - { - var info = GetImageInfo(item, image, null); - - if (info != null) - { - list.Add(info); - } - } - } - - foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) - { - var index = 0; - - // Prevent implicitly captured closure - var currentImageType = imageType; - - foreach (var image in itemImages.Where(i => i.Type == currentImageType)) - { - var info = GetImageInfo(item, image, index); - - if (info != null) - { - list.Add(info); - } - - index++; - } - } - - return list; - } - - private ImageInfo GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) - { - int? width = null; - int? height = null; - string blurhash = null; - long length = 0; - - try - { - if (info.IsLocalFile) - { - var fileInfo = _fileSystem.GetFileInfo(info.Path); - length = fileInfo.Length; - - blurhash = info.BlurHash; - width = info.Width; - height = info.Height; - - if (width <= 0 || height <= 0) - { - width = null; - height = null; - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting image information for {Item}", item.Name); - } - - try - { - return new ImageInfo - { - Path = info.Path, - ImageIndex = imageIndex, - ImageType = info.Type, - ImageTag = _imageProcessor.GetImageCacheTag(item, info), - Size = length, - BlurHash = blurhash, - Width = width, - Height = height - }; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting image information for {Path}", info.Path); - - return null; - } - } - /// /// Gets the specified request. /// @@ -400,56 +189,6 @@ namespace MediaBrowser.Api.Images return GetImage(request, item.Id, item, true); } - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(PostItemImage request) - { - var id = Guid.Parse(GetPathValue(1)); - - request.Type = Enum.Parse(GetPathValue(3).ToString(), true); - - var item = _libraryManager.GetItemById(id); - - return PostImage(item, request.RequestStream, request.Type, Request.ContentType); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(DeleteItemImage request) - { - var item = _libraryManager.GetItemById(request.Id); - - item.DeleteImage(request.Type, request.Index ?? 0); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(UpdateItemImageIndex request) - { - var item = _libraryManager.GetItemById(request.Id); - - UpdateItemIndex(item, request.Type, request.Index, request.NewIndex); - } - - /// - /// Updates the index of the item. - /// - /// The item. - /// The type. - /// Index of the current. - /// The new index. - /// Task. - private void UpdateItemIndex(BaseItem item, ImageType type, int currentIndex, int newIndex) - { - item.SwapImages(type, currentIndex, newIndex); - } - /// /// Gets the image. /// From 10ddbc34ecfc5542f3b32fe3cc4740e30b62cccd Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 18:02:07 -0600 Subject: [PATCH 237/724] Add missing attributes, fix response codes, fix route parameter casing --- .../Controllers/ConfigurationController.cs | 4 +- Jellyfin.Api/Controllers/DevicesController.cs | 1 + .../DisplayPreferencesController.cs | 22 ++---- .../Controllers/ImageByNameController.cs | 6 +- .../Controllers/ItemRefreshController.cs | 15 ++-- .../Controllers/NotificationsController.cs | 8 +- Jellyfin.Api/Controllers/PackageController.cs | 19 ++--- Jellyfin.Api/Controllers/PluginsController.cs | 28 +++++-- .../Controllers/RemoteImageController.cs | 30 ++++---- Jellyfin.Api/Controllers/SessionController.cs | 72 +++++++++--------- .../Controllers/SubtitleController.cs | 54 +++++++------- Jellyfin.Api/Controllers/UserController.cs | 74 +++++++++---------- .../Controllers/VideoAttachmentsController.cs | 2 +- 13 files changed, 172 insertions(+), 163 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 74f1677bdb..d275ed2eba 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers /// Configuration key. /// Configuration returned. /// Configuration. - [HttpGet("Configuration/{Key}")] + [HttpGet("Configuration/{key}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNamedConfiguration([FromRoute] string key) { @@ -81,7 +81,7 @@ namespace Jellyfin.Api.Controllers /// Configuration key. /// Named configuration updated. /// Update status. - [HttpPost("Configuration/{Key}")] + [HttpPost("Configuration/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UpdateNamedConfiguration([FromRoute] string key) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 78368eed60..55ca7b7c0f 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -133,6 +133,7 @@ namespace Jellyfin.Api.Controllers /// A on success, or a if the device could not be found. [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteDevice([FromQuery, BindRequired] string id) { var existingDevice = _deviceManager.GetDevice(id); diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 697a0baf42..56ac215a96 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Threading; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; @@ -34,9 +35,8 @@ namespace Jellyfin.Api.Controllers /// Client. /// Display preferences retrieved. /// An containing the display preferences on success, or a if the display preferences could not be found. - [HttpGet("{DisplayPreferencesId}")] + [HttpGet("{displayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery] [Required] string userId, @@ -52,30 +52,24 @@ namespace Jellyfin.Api.Controllers /// User Id. /// Client. /// New Display Preferences object. - /// Display preferences updated. - /// An on success, or a if the display preferences could not be found. - [HttpPost("{DisplayPreferencesId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + /// Display preferences updated. + /// An on success. + [HttpPost("{displayPreferencesId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] public ActionResult UpdateDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery, BindRequired] string userId, [FromQuery, BindRequired] string client, [FromBody, BindRequired] DisplayPreferences displayPreferences) { - if (displayPreferencesId == null) - { - // TODO - refactor so parameter doesn't exist or is actually used. - } - _displayPreferencesRepository.SaveDisplayPreferences( displayPreferences, userId, client, CancellationToken.None); - return Ok(); + return NoContent(); } } } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 70f46ffa49..0e3c32d3cc 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers /// Image stream retrieved. /// Image not found. /// A containing the image contents on success, or a if the image could not be found. - [HttpGet("General/{Name}/{Type}")] + [HttpGet("General/{name}/{type}")] [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -103,7 +103,7 @@ namespace Jellyfin.Api.Controllers /// Image stream retrieved. /// Image not found. /// A containing the image contents on success, or a if the image could not be found. - [HttpGet("Ratings/{Theme}/{Name}")] + [HttpGet("Ratings/{theme}/{name}")] [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -136,7 +136,7 @@ namespace Jellyfin.Api.Controllers /// Image stream retrieved. /// Image not found. /// A containing the image contents on success, or a if the image could not be found. - [HttpGet("MediaInfo/{Theme}/{Name}")] + [HttpGet("MediaInfo/{theme}/{name}")] [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index 6a16a89c5a..f10f9fb3d8 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using MediaBrowser.Controller.Library; @@ -40,29 +41,29 @@ namespace Jellyfin.Api.Controllers /// /// Refreshes metadata for an item. /// - /// Item id. + /// Item id. /// (Optional) Specifies the metadata refresh mode. /// (Optional) Specifies the image refresh mode. /// (Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh. /// (Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh. /// (Unused) Indicates if the refresh should occur recursively. - /// Item metadata refresh queued. + /// Item metadata refresh queued. /// Item to refresh not found. /// An on success, or a if the item could not be found. - [HttpPost("{Id}/Refresh")] + [HttpPost("{itemId}/Refresh")] [Description("Refreshes metadata for an item.")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")] public ActionResult Post( - [FromRoute] string id, + [FromRoute] Guid itemId, [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None, [FromQuery] bool replaceAllMetadata = false, [FromQuery] bool replaceAllImages = false, [FromQuery] bool recursive = false) { - var item = _libraryManager.GetItemById(id); + var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); @@ -82,7 +83,7 @@ namespace Jellyfin.Api.Controllers }; _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High); - return Ok(); + return NoContent(); } } } diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 01dd23c77f..f226364894 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -42,7 +42,7 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// Notifications returned. /// An containing a list of notifications. - [HttpGet("{UserID}")] + [HttpGet("{userId}")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")] @@ -63,7 +63,7 @@ namespace Jellyfin.Api.Controllers /// The user's ID. /// Summary of user's notifications returned. /// An containing a summary of the users notifications. - [HttpGet("{UserID}/Summary")] + [HttpGet("{userId}/Summary")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult GetNotificationsSummary( @@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers /// A comma-separated list of the IDs of notifications which should be set as read. /// Notifications set as read. /// A . - [HttpPost("{UserID}/Read")] + [HttpPost("{userId}/Read")] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] @@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers /// A comma-separated list of the IDs of notifications which should be set as unread. /// Notifications set as unread. /// A . - [HttpPost("{UserID}/Unread")] + [HttpPost("{userId}/Unread")] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 943c23f8e9..486575d23a 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -35,9 +35,10 @@ namespace Jellyfin.Api.Controllers /// /// The name of the package. /// The GUID of the associated assembly. + /// Package retrieved. /// A containing package information. - [HttpGet("/{Name}")] - [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)] + [HttpGet("/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPackageInfo( [FromRoute] [Required] string name, [FromQuery] string? assemblyGuid) @@ -54,9 +55,10 @@ namespace Jellyfin.Api.Controllers /// /// Gets available packages. /// + /// Available packages returned. /// An containing available packages information. [HttpGet] - [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPackages() { IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); @@ -73,7 +75,7 @@ namespace Jellyfin.Api.Controllers /// Package found. /// Package not found. /// A on success, or a if the package could not be found. - [HttpPost("/Installed/{Name}")] + [HttpPost("/Installed/{name}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = Policies.RequiresElevation)] @@ -102,17 +104,16 @@ namespace Jellyfin.Api.Controllers /// /// Cancels a package installation. /// - /// Installation Id. + /// Installation Id. /// Installation cancelled. /// A on successfully cancelling a package installation. - [HttpDelete("/Installing/{id}")] + [HttpDelete("/Installing/{packageId}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public IActionResult CancelPackageInstallation( - [FromRoute] [Required] string id) + [FromRoute] [Required] Guid packageId) { - _installationManager.CancelInstallation(new Guid(id)); - + _installationManager.CancelInstallation(packageId); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 6075544cf7..8a0913307e 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -11,6 +11,7 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -45,6 +46,7 @@ namespace Jellyfin.Api.Controllers /// Installed plugins returned. /// List of currently installed plugins. [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")] public ActionResult> GetPlugins([FromRoute] bool? isAppStoreEnabled) { @@ -55,11 +57,13 @@ namespace Jellyfin.Api.Controllers /// Uninstalls a plugin. /// /// Plugin id. - /// Plugin uninstalled. + /// Plugin uninstalled. /// Plugin not found. /// An on success, or a if the file could not be found. [HttpDelete("{pluginId}")] [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UninstallPlugin([FromRoute] Guid pluginId) { var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); @@ -69,7 +73,7 @@ namespace Jellyfin.Api.Controllers } _installationManager.UninstallPlugin(plugin); - return Ok(); + return NoContent(); } /// @@ -80,6 +84,8 @@ namespace Jellyfin.Api.Controllers /// Plugin not found or plugin configuration not found. /// Plugin configuration. [HttpGet("{pluginId}/Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPluginConfiguration([FromRoute] Guid pluginId) { if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) @@ -97,14 +103,16 @@ namespace Jellyfin.Api.Controllers /// Accepts plugin configuration as JSON body. /// /// Plugin id. - /// Plugin configuration updated. - /// Plugin not found or plugin does not have configuration. + /// Plugin configuration updated. + /// Plugin not found or plugin does not have configuration. /// /// A that represents the asynchronous operation to update plugin configuration. /// The task result contains an indicating success, or /// when plugin not found or plugin doesn't have configuration. /// [HttpPost("{pluginId}/Configuration")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdatePluginConfiguration([FromRoute] Guid pluginId) { if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) @@ -116,7 +124,7 @@ namespace Jellyfin.Api.Controllers .ConfigureAwait(false); plugin.UpdateConfiguration(configuration); - return Ok(); + return NoContent(); } /// @@ -126,6 +134,7 @@ namespace Jellyfin.Api.Controllers /// Plugin security info. [Obsolete("This endpoint should not be used.")] [HttpGet("SecurityInfo")] + [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetPluginSecurityInfo() { return new PluginSecurityInfo @@ -139,14 +148,15 @@ namespace Jellyfin.Api.Controllers /// Updates plugin security info. /// /// Plugin security info. - /// Plugin security info updated. - /// An . + /// Plugin security info updated. + /// An . [Obsolete("This endpoint should not be used.")] [HttpPost("SecurityInfo")] [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo) { - return Ok(); + return NoContent(); } /// @@ -157,6 +167,7 @@ namespace Jellyfin.Api.Controllers /// Mb registration record. [Obsolete("This endpoint should not be used.")] [HttpPost("RegistrationRecords/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetRegistrationStatus([FromRoute] string name) { return new MBRegistrationRecord @@ -178,6 +189,7 @@ namespace Jellyfin.Api.Controllers /// This endpoint is not implemented. [Obsolete("Paid plugins are not supported")] [HttpGet("/Registrations/{name}")] + [ProducesResponseType(StatusCodes.Status501NotImplemented)] public ActionResult GetRegistration([FromRoute] string name) { // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 80983ee649..41b7f98ee1 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets available remote images for an item. /// - /// Item Id. + /// Item Id. /// The image type. /// 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. @@ -64,18 +64,18 @@ namespace Jellyfin.Api.Controllers /// Remote Images returned. /// Item not found. /// Remote Image Result. - [HttpGet("{Id}/RemoteImages")] + [HttpGet("{itemId}/RemoteImages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetRemoteImages( - [FromRoute] string id, + [FromRoute] Guid itemId, [FromQuery] ImageType? type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string providerName, [FromQuery] bool includeAllLanguages) { - var item = _libraryManager.GetItemById(id); + var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); @@ -123,16 +123,16 @@ namespace Jellyfin.Api.Controllers /// /// Gets available remote image providers for an item. /// - /// Item Id. + /// Item Id. /// Returned remote image providers. /// Item not found. /// List of remote image providers. - [HttpGet("{Id}/RemoteImages/Providers")] + [HttpGet("{itemId}/RemoteImages/Providers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetRemoteImageProviders([FromRoute] string id) + public ActionResult> GetRemoteImageProviders([FromRoute] Guid itemId) { - var item = _libraryManager.GetItemById(id); + var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); @@ -195,21 +195,21 @@ namespace Jellyfin.Api.Controllers /// /// Downloads a remote image for an item. /// - /// Item Id. + /// Item Id. /// The image type. /// The image url. - /// Remote image downloaded. + /// Remote image downloaded. /// Remote image not found. /// Download status. - [HttpPost("{Id}/RemoteImages/Download")] - [ProducesResponseType(StatusCodes.Status200OK)] + [HttpPost("{itemId}/RemoteImages/Download")] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DownloadRemoteImage( - [FromRoute] string id, + [FromRoute] Guid itemId, [FromQuery, BindRequired] ImageType type, [FromQuery] string imageUrl) { - var item = _libraryManager.GetItemById(id); + var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); @@ -219,7 +219,7 @@ namespace Jellyfin.Api.Controllers .ConfigureAwait(false); item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); - return Ok(); + return NoContent(); } /// diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 4f259536a1..315bc9728b 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -113,16 +113,16 @@ namespace Jellyfin.Api.Controllers /// /// Instructs a session to browse to an item or view. /// - /// The session Id. + /// The session Id. /// The type of item to browse to. /// The Id of the item. /// The name of the item. /// Instruction sent to session. /// A . - [HttpPost("/Sessions/{id}/Viewing")] + [HttpPost("/Sessions/{sessionId}/Viewing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DisplayContent( - [FromRoute] string id, + [FromRoute] string sessionId, [FromQuery] string itemType, [FromQuery] string itemId, [FromQuery] string itemName) @@ -136,7 +136,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.SendBrowseCommand( RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, - id, + sessionId, command, CancellationToken.None); @@ -146,17 +146,17 @@ namespace Jellyfin.Api.Controllers /// /// Instructs a session to play an item. /// - /// The session id. + /// The session id. /// The ids of the items to play, comma delimited. /// The starting position of the first item. /// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now. /// The . /// Instruction sent to session. /// A . - [HttpPost("/Sessions/{id}/Playing")] + [HttpPost("/Sessions/{sessionId}/Playing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult Play( - [FromRoute] string id, + [FromRoute] string sessionId, [FromQuery] Guid[] itemIds, [FromQuery] long? startPositionTicks, [FromQuery] PlayCommand playCommand, @@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.SendPlayCommand( RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, - id, + sessionId, playRequest, CancellationToken.None); @@ -183,19 +183,19 @@ namespace Jellyfin.Api.Controllers /// /// Issues a playstate command to a client. /// - /// The session id. + /// The session id. /// The . /// Playstate command sent to session. /// A . - [HttpPost("/Sessions/{id}/Playing/{command}")] + [HttpPost("/Sessions/{sessionId}/Playing/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendPlaystateCommand( - [FromRoute] string id, + [FromRoute] string sessionId, [FromBody] PlaystateRequest playstateRequest) { _sessionManager.SendPlaystateCommand( RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, - id, + sessionId, playstateRequest, CancellationToken.None); @@ -205,14 +205,14 @@ namespace Jellyfin.Api.Controllers /// /// Issues a system command to a client. /// - /// The session id. + /// The session id. /// The command to send. /// System command sent to session. /// A . - [HttpPost("/Sessions/{id}/System/{Command}")] + [HttpPost("/Sessions/{sessionId}/System/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendSystemCommand( - [FromRoute] string id, + [FromRoute] string sessionId, [FromRoute] string command) { var name = command; @@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers ControllingUserId = currentSession.UserId }; - _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None); + _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); return NoContent(); } @@ -236,14 +236,14 @@ namespace Jellyfin.Api.Controllers /// /// Issues a general command to a client. /// - /// The session id. + /// The session id. /// The command to send. /// General command sent to session. /// A . - [HttpPost("/Sessions/{id}/Command/{Command}")] + [HttpPost("/Sessions/{sessionId}/Command/{Command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendGeneralCommand( - [FromRoute] string id, + [FromRoute] string sessionId, [FromRoute] string command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers ControllingUserId = currentSession.UserId }; - _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None); + _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); return NoContent(); } @@ -262,14 +262,14 @@ namespace Jellyfin.Api.Controllers /// /// Issues a full general command to a client. /// - /// The session id. + /// The session id. /// The . /// Full general command sent to session. /// A . - [HttpPost("/Sessions/{id}/Command")] + [HttpPost("/Sessions/{sessionId}/Command")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendFullGeneralCommand( - [FromRoute] string id, + [FromRoute] string sessionId, [FromBody, Required] GeneralCommand command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -283,7 +283,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.SendGeneralCommand( currentSession.Id, - id, + sessionId, command, CancellationToken.None); @@ -293,16 +293,16 @@ namespace Jellyfin.Api.Controllers /// /// Issues a command to a client to display a message to the user. /// - /// The session id. + /// The session id. /// The message test. /// The message header. /// The message timeout. If omitted the user will have to confirm viewing the message. /// Message sent. /// A . - [HttpPost("/Sessions/{id}/Message")] + [HttpPost("/Sessions/{sessionId}/Message")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendMessageCommand( - [FromRoute] string id, + [FromRoute] string sessionId, [FromQuery] string text, [FromQuery] string header, [FromQuery] long? timeoutMs) @@ -314,7 +314,7 @@ namespace Jellyfin.Api.Controllers Text = text }; - _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, command, CancellationToken.None); + _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None); return NoContent(); } @@ -322,34 +322,34 @@ namespace Jellyfin.Api.Controllers /// /// Adds an additional user to a session. /// - /// The session id. + /// The session id. /// The user id. /// User added to session. /// A . - [HttpPost("/Sessions/{id}/User/{userId}")] + [HttpPost("/Sessions/{sessionId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( - [FromRoute] string id, + [FromRoute] string sessionId, [FromRoute] Guid userId) { - _sessionManager.AddAdditionalUser(id, userId); + _sessionManager.AddAdditionalUser(sessionId, userId); return NoContent(); } /// /// Removes an additional user from a session. /// - /// The session id. + /// The session id. /// The user id. /// User removed from session. /// A . - [HttpDelete("/Sessions/{id}/User/{userId}")] + [HttpDelete("/Sessions/{sessionId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( - [FromRoute] string id, + [FromRoute] string sessionId, [FromRoute] Guid userId) { - _sessionManager.RemoveAdditionalUser(id, userId); + _sessionManager.RemoveAdditionalUser(sessionId, userId); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 74ec5f9b52..95cc39524c 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -75,20 +75,20 @@ namespace Jellyfin.Api.Controllers /// /// Deletes an external subtitle file. /// - /// The item id. + /// The item id. /// The index of the subtitle file. /// Subtitle deleted. /// Item not found. /// A . - [HttpDelete("/Videos/{id}/Subtitles/{index}")] + [HttpDelete("/Videos/{itemId}/Subtitles/{index}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteSubtitle( - [FromRoute] Guid id, + [FromRoute] Guid itemId, [FromRoute] int index) { - var item = _libraryManager.GetItemById(id); + var item = _libraryManager.GetItemById(itemId); if (item == null) { @@ -102,20 +102,20 @@ namespace Jellyfin.Api.Controllers /// /// Search remote subtitles. /// - /// The item id. + /// The item id. /// The language of the subtitles. /// Optional. Only show subtitles which are a perfect match. /// Subtitles retrieved. /// An array of . - [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")] + [HttpGet("/Items/{itemId}/RemoteSearch/Subtitles/{language}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( - [FromRoute] Guid id, + [FromRoute] Guid itemId, [FromRoute] string language, [FromQuery] bool? isPerfectMatch) { - var video = (Video)_libraryManager.GetItemById(id); + var video = (Video)_libraryManager.GetItemById(itemId); return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false); } @@ -123,18 +123,18 @@ namespace Jellyfin.Api.Controllers /// /// Downloads a remote subtitle. /// - /// The item id. + /// The item id. /// The subtitle id. /// Subtitle downloaded. /// A . - [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")] + [HttpPost("/Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( - [FromRoute] Guid id, + [FromRoute] Guid itemId, [FromRoute] string subtitleId) { - var video = (Video)_libraryManager.GetItemById(id); + var video = (Video)_libraryManager.GetItemById(itemId); try { @@ -171,28 +171,28 @@ namespace Jellyfin.Api.Controllers /// /// Gets subtitles in a specified format. /// - /// The item id. + /// The item id. /// The media source id. /// The subtitle stream index. /// The format of the returned subtitle. - /// Optional. The start position of the subtitle in ticks. /// Optional. The end position of the subtitle in ticks. /// Optional. Whether to copy the timestamps. /// Optional. Whether to add a VTT time map. + /// Optional. The start position of the subtitle in ticks. /// File returned. /// A with the subtitle file. - [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] - [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")] + [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] + [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitle( - [FromRoute, Required] Guid id, + [FromRoute, Required] Guid itemId, [FromRoute, Required] string mediaSourceId, [FromRoute, Required] int index, [FromRoute, Required] string format, - [FromRoute] long startPositionTicks, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps, - [FromQuery] bool addVttTimeMap) + [FromQuery] bool addVttTimeMap, + [FromRoute] long startPositionTicks = 0) { if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) { @@ -201,9 +201,9 @@ namespace Jellyfin.Api.Controllers if (string.IsNullOrEmpty(format)) { - var item = (Video)_libraryManager.GetItemById(id); + var item = (Video)_libraryManager.GetItemById(itemId); - var idString = id.ToString("N", CultureInfo.InvariantCulture); + var idString = itemId.ToString("N", CultureInfo.InvariantCulture); var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); @@ -216,7 +216,7 @@ namespace Jellyfin.Api.Controllers if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) { - await using Stream stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); using var reader = new StreamReader(stream); var text = await reader.ReadToEndAsync().ConfigureAwait(false); @@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers return File( await EncodeSubtitles( - id, + itemId, mediaSourceId, index, format, @@ -241,23 +241,23 @@ namespace Jellyfin.Api.Controllers /// /// Gets an HLS subtitle playlist. /// - /// The item id. + /// The item id. /// The subtitle stream index. /// The media source id. /// The subtitle segment length. /// Subtitle playlist retrieved. /// A with the HLS subtitle playlist. - [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] + [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task GetSubtitlePlaylist( - [FromRoute] Guid id, + [FromRoute] Guid itemId, [FromRoute] int index, [FromRoute] string mediaSourceId, [FromQuery, Required] int segmentLength) { - var item = (Video)_libraryManager.GetItemById(id); + var item = (Video)_libraryManager.GetItemById(itemId); var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 68ab5813ce..0d57dcc837 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -105,17 +105,17 @@ namespace Jellyfin.Api.Controllers /// /// Gets a user by Id. /// - /// The user id. + /// The user id. /// User returned. /// User not found. /// An with information about the user or a if the user was not found. - [HttpGet("{id}")] + [HttpGet("{userId}")] [Authorize(Policy = Policies.IgnoreSchedule)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetUserById([FromRoute] Guid id) + public ActionResult GetUserById([FromRoute] Guid userId) { - var user = _userManager.GetUserById(id); + var user = _userManager.GetUserById(userId); if (user == null) { @@ -129,17 +129,17 @@ namespace Jellyfin.Api.Controllers /// /// Deletes a user. /// - /// The user id. + /// The user id. /// User deleted. /// User not found. /// A indicating success or a if the user was not found. - [HttpDelete("{id}")] + [HttpDelete("{userId}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteUser([FromRoute] Guid id) + public ActionResult DeleteUser([FromRoute] Guid userId) { - var user = _userManager.GetUserById(id); + var user = _userManager.GetUserById(userId); if (user == null) { @@ -154,23 +154,23 @@ namespace Jellyfin.Api.Controllers /// /// Authenticates a user. /// - /// The user id. + /// The user id. /// The password as plain text. /// The password sha1-hash. /// User authenticated. /// Sha1-hashed password only is not allowed. /// User not found. /// A containing an . - [HttpPost("{id}/Authenticate")] + [HttpPost("{userId}/Authenticate")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( - [FromRoute, Required] Guid id, + [FromRoute, Required] Guid userId, [FromQuery, BindRequired] string pw, [FromQuery, BindRequired] string password) { - var user = _userManager.GetUserById(id); + var user = _userManager.GetUserById(userId); if (user == null) { @@ -230,27 +230,27 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's password. /// - /// The user id. + /// The user id. /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. /// A indicating success or a or a on failure. - [HttpPost("{id}/Password")] + [HttpPost("{userId}/Password")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( - [FromRoute] Guid id, + [FromRoute] Guid userId, [FromBody] UpdateUserPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { return Forbid("User is not allowed to update the password."); } - var user = _userManager.GetUserById(id); + var user = _userManager.GetUserById(userId); if (user == null) { @@ -288,27 +288,27 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's easy password. /// - /// The user id. + /// The user id. /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. /// A indicating success or a or a on failure. - [HttpPost("{id}/EasyPassword")] + [HttpPost("{userId}/EasyPassword")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateUserEasyPassword( - [FromRoute] Guid id, + [FromRoute] Guid userId, [FromBody] UpdateUserEasyPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { return Forbid("User is not allowed to update the easy password."); } - var user = _userManager.GetUserById(id); + var user = _userManager.GetUserById(userId); if (user == null) { @@ -330,19 +330,19 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user. /// - /// The user id. + /// The user id. /// The updated user model. /// User updated. /// User information was not supplied. /// User update forbidden. /// A indicating success or a or a on failure. - [HttpPost("{id}")] + [HttpPost("{userId}")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task UpdateUser( - [FromRoute] Guid id, + [FromRoute] Guid userId, [FromBody] UserDto updateUser) { if (updateUser == null) @@ -350,12 +350,12 @@ namespace Jellyfin.Api.Controllers return BadRequest(); } - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false)) + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) { return Forbid("User update not allowed."); } - var user = _userManager.GetUserById(id); + var user = _userManager.GetUserById(userId); if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) { @@ -374,19 +374,19 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user policy. /// - /// The user id. + /// The user id. /// The new user policy. /// User policy updated. /// User policy was not supplied. /// User policy update forbidden. /// A indicating success or a or a on failure.. - [HttpPost("{id}/Policy")] + [HttpPost("{userId}/Policy")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult UpdateUserPolicy( - [FromRoute] Guid id, + [FromRoute] Guid userId, [FromBody] UserPolicy newPolicy) { if (newPolicy == null) @@ -394,7 +394,7 @@ namespace Jellyfin.Api.Controllers return BadRequest(); } - var user = _userManager.GetUserById(id); + var user = _userManager.GetUserById(userId); // If removing admin access if (!(newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))) @@ -423,7 +423,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.RevokeUserTokens(user.Id, currentToken); } - _userManager.UpdatePolicy(id, newPolicy); + _userManager.UpdatePolicy(userId, newPolicy); return NoContent(); } @@ -431,25 +431,25 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user configuration. /// - /// The user id. + /// The user id. /// The new user configuration. /// User configuration updated. /// User configuration update forbidden. /// A indicating success. - [HttpPost("{id}/Configuration")] + [HttpPost("{userId}/Configuration")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult UpdateUserConfiguration( - [FromRoute] Guid id, + [FromRoute] Guid userId, [FromBody] UserConfiguration userConfig) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false)) + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) { return Forbid("User configuration update not allowed"); } - _userManager.UpdateConfiguration(id, userConfig); + _userManager.UpdateConfiguration(userId, userConfig); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 2528fd75d0..943ba8af3d 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -44,7 +44,7 @@ namespace Jellyfin.Api.Controllers /// 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}")] + [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] From 9a223b7359305ab718b744394688e1d948b56686 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 21 Jun 2020 12:35:06 +0200 Subject: [PATCH 238/724] Fix suggestions --- Jellyfin.Api/Controllers/DashboardController.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 6a7bf7d0aa..21c320a490 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); - if (pageType != null) + if (pageType.HasValue) { configPages = configPages.Where(p => p.ConfigurationPageType == pageType).ToList(); } @@ -246,14 +246,12 @@ namespace Jellyfin.Api.Controllers private IEnumerable> GetPluginPages(IPlugin plugin) { - var hasConfig = plugin as IHasWebPages; - - if (hasConfig == null) + if (!(plugin is IHasWebPages)) { return new List>(); } - return hasConfig.GetPages().Select(i => new Tuple(i, plugin)); + return (plugin as IHasWebPages)!.GetPages().Select(i => new Tuple(i, plugin)); } private IEnumerable> GetPluginPages() From 8f9c9859882815d10e51ad5c2116d516a1cb89f4 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 21 Jun 2020 16:00:16 +0200 Subject: [PATCH 239/724] Move VideosService to Jellyfin.Api --- Jellyfin.Api/Controllers/VideosController.cs | 202 +++++++++++++++++++ MediaBrowser.Api/VideosService.cs | 193 ------------------ 2 files changed, 202 insertions(+), 193 deletions(-) create mode 100644 Jellyfin.Api/Controllers/VideosController.cs delete mode 100644 MediaBrowser.Api/VideosService.cs diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs new file mode 100644 index 0000000000..532ce59c50 --- /dev/null +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -0,0 +1,202 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The videos controller. + /// + [Route("Videos")] + public class VideosController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public VideosController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } + + /// + /// Gets additional parts for a video. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Additional parts returned. + /// A with the parts. + [HttpGet("{itemId}/AdditionalParts")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid userId) + { + var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + + var item = itemId.Equals(Guid.Empty) + ? (!userId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.RootFolder) + : _libraryManager.GetItemById(itemId); + + var dtoOptions = new DtoOptions(); + dtoOptions = dtoOptions.AddClientFields(Request); + + BaseItemDto[] items; + if (item is Video video) + { + items = video.GetAdditionalParts() + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) + .ToArray(); + } + else + { + items = Array.Empty(); + } + + var result = new QueryResult + { + Items = items, + TotalRecordCount = items.Length + }; + + return result; + } + + /// + /// Removes alternate video sources. + /// + /// The item id. + /// Alternate sources deleted. + /// Video not found. + /// A indicating success, or a if the video doesn't exist. + [HttpDelete("{itemId}/AlternateSources")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteAlternateSources([FromRoute] Guid itemId) + { + var video = (Video)_libraryManager.GetItemById(itemId); + + if (video == null) + { + return NotFound("The video either does not exist or the id does not belong to a video."); + } + + foreach (var link in video.GetLinkedAlternateVersions()) + { + link.SetPrimaryVersionId(null); + link.LinkedAlternateVersions = Array.Empty(); + + link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + } + + video.LinkedAlternateVersions = Array.Empty(); + video.SetPrimaryVersionId(null); + video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + + return NoContent(); + } + + /// + /// Merges videos into a single record. + /// + /// Item id list. This allows multiple, comma delimited. + /// Videos merged. + /// Supply at least 2 video ids. + /// A indicating success, or a if less than two ids were supplied. + [HttpPost("MergeVersions")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public ActionResult MergeVersions([FromQuery] string itemIds) + { + var items = RequestHelpers.Split(itemIds, ',', true) + .Select(i => _libraryManager.GetItemById(i)) + .OfType