diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
49 files changed, 1189 insertions, 892 deletions
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index fed7ed3e5..154a56702 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -3,8 +3,10 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -75,6 +77,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Total record count.</param> /// <response code="200">Artists returned.</response> @@ -88,8 +92,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, @@ -110,6 +114,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -127,8 +133,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, @@ -148,7 +154,8 @@ namespace Jellyfin.Api.Controllers MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) @@ -274,6 +281,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Total record count.</param> /// <response code="200">Album artists returned.</response> @@ -287,8 +296,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, @@ -309,6 +318,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -326,8 +337,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, @@ -347,7 +358,8 @@ namespace Jellyfin.Api.Controllers MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 616fe5b91..a6e70e72d 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -168,22 +168,22 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -333,22 +333,22 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index b70c76e80..54bd80095 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -121,9 +120,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery] string? sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] string? sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { var user = userId.HasValue && !userId.Equals(Guid.Empty) diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 2a7b2b5c6..852d1e9cb 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index e1c9f69f6..b6309baab 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text.Json; @@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers private readonly IServerConfigurationManager _configurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options; /// <summary> /// Initializes a new instance of the <see cref="ConfigurationController"/> class. @@ -94,6 +95,11 @@ namespace Jellyfin.Api.Controllers { var configurationType = _configurationManager.GetConfigurationType(key); var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false); + if (configuration == null) + { + throw new ArgumentException("Body doesn't contain a valid configuration"); + } + _configurationManager.SaveConfiguration(key, configuration); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index ccc81dfc5..445733c24 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -6,18 +6,11 @@ using System.Net.Mime; using Jellyfin.Api.Attributes; 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.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers { @@ -28,67 +21,35 @@ namespace Jellyfin.Api.Controllers public class DashboardController : BaseJellyfinApiController { private readonly ILogger<DashboardController> _logger; - private readonly IServerApplicationHost _appHost; + private readonly IPluginManager _pluginManager; /// <summary> /// Initializes a new instance of the <see cref="DashboardController"/> class. /// </summary> /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> - /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> + /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param> public DashboardController( ILogger<DashboardController> logger, - IServerApplicationHost appHost) + IPluginManager pluginManager) { _logger = logger; - _appHost = appHost; + _pluginManager = pluginManager; } /// <summary> /// Gets the configuration pages. /// </summary> /// <param name="enableInMainMenu">Whether to enable in the main menu.</param> - /// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param> /// <response code="200">ConfigurationPages returned.</response> /// <response code="404">Server still loading.</response> /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns> [HttpGet("web/ConfigurationPages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages( - [FromQuery] bool? enableInMainMenu, - [FromQuery] ConfigurationPageType? pageType) + public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages( + [FromQuery] bool? enableInMainMenu) { - const string unavailableMessage = "The server is still loading. Please try again momentarily."; - - var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList(); - - if (pages == null) - { - return NotFound(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 (pageType.HasValue) - { - configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList(); - } + var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList(); if (enableInMainMenu.HasValue) { @@ -111,68 +72,42 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")] public ActionResult GetDashboardConfigurationPage([FromQuery] string? name) { - IPlugin? plugin = null; - Stream? stream = null; - - var isJs = false; - var isTemplate = false; - - var page = _appHost.GetExports<IPluginConfigurationPage>().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) { - 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); - } + return NotFound(); } - if (plugin != null && stream != null) + IPlugin plugin = altPage.Item2; + string resourcePath = altPage.Item1.EmbeddedResourcePath; + Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath); + if (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")); + _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name); + return NotFound(); } - return NotFound(); + return File(stream, MimeTypes.GetMimeType(resourcePath)); } - private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin) + private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin) { - return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); + return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); } - private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin) + private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin) { - if (!(plugin is IHasWebPages hasWebPages)) + if (plugin.Instance is not IHasWebPages hasWebPages) { - return new List<Tuple<PluginPageInfo, IPlugin>>(); + return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>(); } - return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin)); + return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance)); } private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() { - return _appHost.Plugins.SelectMany(GetPluginPages); + return _pluginManager.Plugins.SelectMany(GetPluginPages); } } } diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 8b8f63015..87b4577b6 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -12,6 +12,7 @@ using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesManager _displayPreferencesManager; + private readonly ILogger<DisplayPreferencesController> _logger; /// <summary> /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class. /// </summary> /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param> - public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager) + /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param> + public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger) { _displayPreferencesManager = displayPreferencesManager; + _logger = logger; } /// <summary> @@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers { Client = displayPreferences.Client, Id = displayPreferences.ItemId.ToString(), - ViewType = itemPreferences.ViewType.ToString(), SortBy = itemPreferences.SortBy, SortOrder = itemPreferences.SortOrder, IndexBy = displayPreferences.IndexBy?.ToString(), @@ -77,16 +80,12 @@ namespace Jellyfin.Api.Controllers dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant(); } - foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client)) - { - dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant(); - } - dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant(); dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["tvhome"] = displayPreferences.TvHome; + dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme; // Load all custom display preferences var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); @@ -189,10 +188,9 @@ namespace Jellyfin.Api.Controllers foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) { - if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId)) + if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type)) { - var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client); - itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType); + _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]); displayPreferences.CustomPrefs.Remove(key); } } @@ -204,11 +202,6 @@ namespace Jellyfin.Api.Controllers itemPrefs.RememberSorting = displayPreferences.RememberSorting; itemPrefs.ItemId = itemId; - if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType)) - { - itemPrefs.ViewType = viewType; - } - // Set all remaining custom preferences. _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs); _displayPreferencesManager.SaveChanges(); diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 4fd9c2fbf..694d16ad9 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Description xml returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>An <see cref="OkResult"/> containing the description xml.</returns> [HttpGet("{serverId}/description")] [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) { - var url = GetAbsoluteUri(); - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); - return Ok(xml); + if (DlnaEntryPoint.Enabled) + { + var url = GetAbsoluteUri(); + var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); + return Ok(xml); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Dlna content directory returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> [HttpGet("{serverId}/ContentDirectory")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetContentDirectory([FromRoute, Required] string serverId) { - return Ok(_contentDirectory.GetServiceXml()); + if (DlnaEntryPoint.Enabled) + { + return Ok(_contentDirectory.GetServiceXml()); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Dlna media receiver registrar xml returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Dlna media receiver registrar xml.</returns> [HttpGet("{serverId}/MediaReceiverRegistrar")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) { - return Ok(_mediaReceiverRegistrar.GetServiceXml()); + if (DlnaEntryPoint.Enabled) + { + return Ok(_mediaReceiverRegistrar.GetServiceXml()); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Dlna media receiver registrar xml returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Dlna media receiver registrar xml.</returns> [HttpGet("{serverId}/ConnectionManager")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetConnectionManager([FromRoute, Required] string serverId) { - return Ok(_connectionManager.GetServiceXml()); + if (DlnaEntryPoint.Enabled) + { + return Ok(_connectionManager.GetServiceXml()); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Control response.</returns> [HttpPost("{serverId}/ContentDirectory/Control")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); + if (DlnaEntryPoint.Enabled) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Control response.</returns> [HttpPost("{serverId}/ConnectionManager/Control")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); + if (DlnaEntryPoint.Enabled) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Control response.</returns> [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); + if (DlnaEntryPoint.Enabled) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Event subscription response.</returns> [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) { - return ProcessEventRequest(_mediaReceiverRegistrar); + if (DlnaEntryPoint.Enabled) + { + return ProcessEventRequest(_mediaReceiverRegistrar); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Event subscription response.</returns> [HttpSubscribe("{serverId}/ContentDirectory/Events")] [HttpUnsubscribe("{serverId}/ContentDirectory/Events")] [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) { - return ProcessEventRequest(_contentDirectory); + if (DlnaEntryPoint.Enabled) + { + return ProcessEventRequest(_contentDirectory); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Event subscription response.</returns> [HttpSubscribe("{serverId}/ConnectionManager/Events")] [HttpUnsubscribe("{serverId}/ConnectionManager/Events")] [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) { - return ProcessEventRequest(_connectionManager); + if (DlnaEntryPoint.Enabled) + { + return ProcessEventRequest(_connectionManager); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <param name="fileName">The icon filename.</param> + /// <response code="200">Request processed.</response> + /// <response code="404">Not Found.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Icon stream.</returns> [HttpGet("{serverId}/icons/{fileName}")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [ProducesImageFile] public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) { - return GetIconInternal(fileName); + if (DlnaEntryPoint.Enabled) + { + return GetIconInternal(fileName); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="fileName">The icon filename.</param> /// <returns>Icon stream.</returns> + /// <response code="200">Request processed.</response> + /// <response code="404">Not Found.</response> + /// <response code="503">DLNA is disabled.</response> [HttpGet("icons/{fileName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [ProducesImageFile] public ActionResult GetIcon([FromRoute, Required] string fileName) { - return GetIconInternal(fileName); + if (DlnaEntryPoint.Enabled) + { + return GetIconInternal(fileName); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } private ActionResult GetIconInternal(string fileName) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 6e85737d2..555062e55 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -15,7 +15,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -29,7 +28,6 @@ using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -53,8 +51,6 @@ namespace Jellyfin.Api.Controllers private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<DynamicHlsController> _logger; @@ -73,12 +69,11 @@ namespace Jellyfin.Api.Controllers /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, @@ -88,15 +83,12 @@ namespace Jellyfin.Api.Controllers IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, ILogger<DynamicHlsController> logger, - DynamicHlsHelper dynamicHlsHelper) + DynamicHlsHelper dynamicHlsHelper, + EncodingHelper encodingHelper) { - _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); - _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; @@ -105,12 +97,12 @@ namespace Jellyfin.Api.Controllers _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; + _encodingHelper = encodingHelper; + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } @@ -204,7 +196,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -219,14 +211,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { var streamingRequest = new HlsVideoRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -250,28 +242,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; @@ -371,7 +363,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -386,14 +378,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { var streamingRequest = new HlsAudioRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -417,28 +409,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; @@ -534,7 +526,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -549,14 +541,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -580,28 +572,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -699,7 +691,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -714,14 +706,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -745,28 +737,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -869,7 +861,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -884,14 +876,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var streamingRequest = new VideoRequestDto { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -915,28 +907,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -1041,7 +1033,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -1056,14 +1048,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var streamingRequest = new StreamingRequestDto { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -1087,28 +1079,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -1127,9 +1119,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -1147,7 +1137,7 @@ namespace Jellyfin.Api.Controllers var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase); var hlsVersion = isHlsInFmp4 ? "7" : "3"; - var builder = new StringBuilder(); + var builder = new StringBuilder(128); builder.AppendLine("#EXTM3U") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") @@ -1200,7 +1190,7 @@ namespace Jellyfin.Api.Controllers throw new ArgumentException("StartTimeTicks is not allowed."); } - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; using var state = await StreamingHelpers.GetStreamingState( @@ -1212,14 +1202,12 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, TranscodingJobType, - cancellationTokenSource.Token) + cancellationToken) .ConfigureAwait(false); var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); @@ -1238,7 +1226,7 @@ namespace Jellyfin.Api.Controllers } var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); - await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); var released = false; var startTranscoding = false; @@ -1334,24 +1322,28 @@ namespace Jellyfin.Api.Controllers return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } - private double[] GetSegmentLengths(StreamState state) - { - var result = new List<double>(); + private static double[] GetSegmentLengths(StreamState state) + => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength); - var ticks = state.RunTimeTicks ?? 0; - - var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks; + internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength) + { + var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks; + var wholeSegments = runtimeTicks / segmentLengthTicks; + var remainingTicks = runtimeTicks % segmentLengthTicks; - while (ticks > 0) + var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1); + var segments = new double[segmentsLen]; + for (int i = 0; i < wholeSegments; i++) { - var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks; - - result.Add(TimeSpan.FromTicks(length).TotalSeconds); + segments[i] = segmentlength; + } - ticks -= length; + if (remainingTicks != 0) + { + segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds; } - return result.ToArray(); + return segments; } private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber) @@ -1387,18 +1379,13 @@ namespace Jellyfin.Api.Controllers } else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) { - var outputFmp4HeaderArg = string.Empty; - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - if (isWindows) + var outputFmp4HeaderArg = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) switch { // on Windows, the path of fmp4 header file needs to be configured - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; - } - else - { + true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"", // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""; - } + false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" + }; segmentFormat = "fmp4" + outputFmp4HeaderArg; } @@ -1773,9 +1760,9 @@ namespace Jellyfin.Api.Controllers private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem) { - var folder = Path.GetDirectoryName(playlist); + var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist)); - var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty; + var filePrefix = Path.GetFileNameWithoutExtension(playlist); try { diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 9220b988f..223b2a2b6 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,13 +1,12 @@ using System; using System.Linq; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; 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; @@ -51,7 +50,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { var user = userId.HasValue && !userId.Equals(Guid.Empty) @@ -60,10 +59,10 @@ namespace Jellyfin.Api.Controllers BaseItem? item = null; if (includeItemTypes.Length != 1 - || !(string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase))) + || !(includeItemTypes[0] == BaseItemKind.BoxSet + || includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.Trailer + || includeItemTypes[0] == BaseItemKind.Program)) { item = _libraryManager.GetParentItem(parentId, user?.Id); } @@ -72,7 +71,7 @@ namespace Jellyfin.Api.Controllers { User = user, MediaTypes = mediaTypes, - IncludeItemTypes = includeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), Recursive = true, EnableTotalRecordCount = false, DtoOptions = new DtoOptions @@ -137,7 +136,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<QueryFilters> GetQueryFilters( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isAiring, [FromQuery] bool? isMovie, [FromQuery] bool? isSports, @@ -152,10 +151,10 @@ namespace Jellyfin.Api.Controllers BaseItem? parentItem = null; if (includeItemTypes.Length == 1 - && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase))) + && (includeItemTypes[0] == BaseItemKind.BoxSet + || includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.Trailer + || includeItemTypes[0] == BaseItemKind.Program)) { parentItem = null; } @@ -167,7 +166,7 @@ namespace Jellyfin.Api.Controllers var filters = new QueryFilters(); var genreQuery = new InternalItemsQuery(user) { - IncludeItemTypes = includeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), DtoOptions = new DtoOptions { Fields = Array.Empty<ItemFields>(), @@ -192,10 +191,10 @@ namespace Jellyfin.Api.Controllers } if (includeItemTypes.Length == 1 - && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase))) + && (includeItemTypes[0] == BaseItemKind.MusicAlbum + || includeItemTypes[0] == BaseItemKind.MusicVideo + || includeItemTypes[0] == BaseItemKind.MusicArtist + || includeItemTypes[0] == BaseItemKind.Audio)) { filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair { diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index b6755ed5e..5aa457153 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -62,6 +63,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> /// <response code="200">Genres returned.</response> @@ -74,8 +77,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -83,6 +86,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -96,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -106,7 +111,8 @@ namespace Jellyfin.Api.Controllers NameStartsWithOrGreater = nameStartsWithOrGreater, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index f51987732..473bdc523 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -2,13 +2,11 @@ using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; @@ -63,7 +61,13 @@ namespace Jellyfin.Api.Controllers { // TODO: Deprecate with new iOS app var file = segmentId + Path.GetExtension(Request.Path); - file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath)) + { + return BadRequest("Invalid segment."); + } return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext); } @@ -83,7 +87,13 @@ namespace Jellyfin.Api.Controllers public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) { var file = playlistId + Path.GetExtension(Request.Path); - file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8") + { + return BadRequest("Invalid segment."); + } return GetFileResult(file, file); } @@ -98,7 +108,9 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Videos/ActiveEncodings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId) + public ActionResult StopEncodingProcess( + [FromQuery, Required] string deviceId, + [FromQuery, Required] string playSessionId) { _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); return NoContent(); @@ -130,7 +142,12 @@ namespace Jellyfin.Api.Controllers var file = segmentId + Path.GetExtension(Request.Path); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); - file = Path.Combine(transcodeFolderPath, file); + file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath)) + { + return BadRequest("Invalid segment."); + } var normalizedPlaylistId = playlistId; diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 198dbc51f..e1b808098 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers : type; var path = BaseItem.SupportedImageExtensions - .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)) + .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i))) .FirstOrDefault(System.IO.File.Exists); if (path == null) @@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers return NotFound(); } + if (!path.StartsWith(_applicationPaths.GeneralPath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); return File(System.IO.File.OpenRead(path), contentType); } @@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> private ActionResult GetImageFile(string basePath, string theme, string? name) { - var themeFolder = Path.Combine(basePath, theme); + var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme)); + if (Directory.Exists(themeFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) @@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { + if (!path.StartsWith(basePath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); + return PhysicalFile(path, contentType); } } - var allFolder = Path.Combine(basePath, "all"); + var allFolder = Path.GetFullPath(Path.Combine(basePath, "all")); if (Directory.Exists(allFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) @@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { + if (!path.StartsWith(basePath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); return PhysicalFile(path, contentType); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index e828a0801..8f7500ac6 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -87,6 +87,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Users/{userId}/Images/{imageType}")] [Authorize(Policy = Policies.DefaultAuthorization)] + [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] @@ -112,7 +113,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Users/{userId}/Images/{imageType}/{index}")] [Authorize(Policy = Policies.DefaultAuthorization)] + [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] @@ -158,7 +160,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -194,6 +196,11 @@ namespace Jellyfin.Api.Controllers } var user = _userManager.GetUserById(userId); + if (user?.ProfileImage == null) + { + return NoContent(); + } + try { System.IO.File.Delete(user.ProfileImage.Path); @@ -233,6 +240,11 @@ namespace Jellyfin.Api.Controllers } var user = _userManager.GetUserById(userId); + if (user?.ProfileImage == null) + { + return NoContent(); + } + try { System.IO.File.Delete(user.ProfileImage.Path); @@ -312,6 +324,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns> [HttpPost("Items/{itemId}/Images/{imageType}")] [Authorize(Policy = Policies.RequiresElevation)] + [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -325,9 +338,11 @@ namespace Jellyfin.Api.Controllers return NotFound(); } + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + // 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); + await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); @@ -344,6 +359,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns> [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")] [Authorize(Policy = Policies.RequiresElevation)] + [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -358,9 +374,11 @@ namespace Jellyfin.Api.Controllers return NotFound(); } + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + // 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); + await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); @@ -384,7 +402,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] int newIndex) + [FromQuery, Required] int newIndex) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -472,6 +490,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param> @@ -501,8 +521,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromQuery] string? tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, @@ -531,7 +553,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -552,6 +575,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param> @@ -581,8 +606,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromQuery] string? tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, @@ -610,7 +637,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -630,6 +658,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param> @@ -659,8 +689,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromRoute, Required] string tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromRoute, Required] ImageFormat format, [FromQuery] bool? addPlayedIndicator, [FromRoute, Required] double percentPlayed, @@ -689,7 +721,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -713,6 +746,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -733,7 +768,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetArtistImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -742,7 +777,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -768,7 +805,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -792,6 +830,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -812,7 +852,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetGenreImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -821,7 +861,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -847,7 +889,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -872,6 +915,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -892,7 +937,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -901,7 +946,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -926,7 +973,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -950,6 +998,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -970,7 +1020,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetMusicGenreImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -979,7 +1029,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1005,7 +1057,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1030,6 +1083,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1050,7 +1105,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -1059,7 +1114,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1084,7 +1141,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1108,6 +1166,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1128,7 +1188,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetPersonImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -1137,7 +1197,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1163,7 +1225,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1188,6 +1251,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1208,7 +1273,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -1217,7 +1282,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1242,7 +1309,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1266,6 +1334,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1295,7 +1365,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1321,7 +1393,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1346,6 +1419,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1375,7 +1450,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1400,7 +1477,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1424,6 +1502,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1453,7 +1533,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1461,7 +1543,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageIndex) { var user = _userManager.GetUserById(userId); - if (user == null) + if (user?.ProfileImage == null) { return NotFound(); } @@ -1496,7 +1578,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1522,6 +1605,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1551,7 +1636,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1593,7 +1680,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1677,7 +1765,8 @@ namespace Jellyfin.Api.Controllers int? width, int? height, int? quality, - bool? cropWhitespace, + int? fillWidth, + int? fillHeight, bool? addPlayedIndicator, int? blur, string? backgroundColor, @@ -1719,8 +1808,6 @@ namespace Jellyfin.Api.Controllers } } - cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art; - var outputFormats = GetOutputFormats(format); TimeSpan? cacheDuration = null; @@ -1740,11 +1827,13 @@ namespace Jellyfin.Api.Controllers item, itemId, imageIndex, + width, height, - maxHeight, maxWidth, + maxHeight, + fillWidth, + fillHeight, quality, - width, addPlayedIndicator, percentPlayed, unplayedCount, @@ -1752,7 +1841,6 @@ namespace Jellyfin.Api.Controllers backgroundColor, foregroundLayer, imageInfo, - cropWhitespace.Value, outputFormats, cacheDuration, responseHeaders, @@ -1771,17 +1859,15 @@ namespace Jellyfin.Api.Controllers private ImageFormat[] GetClientSupportedFormats() { - var acceptTypes = Request.Headers[HeaderNames.Accept]; - var supportedFormats = new List<string>(); - if (acceptTypes.Count > 0) + var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); + for (var i = 0; i < supportedFormats.Length; i++) { - foreach (var type in acceptTypes) + // Remove charsets etc. (anything after semi-colon) + var type = supportedFormats[i]; + int index = type.IndexOf(';', StringComparison.Ordinal); + if (index != -1) { - int index = type.IndexOf(';', StringComparison.Ordinal); - if (index != -1) - { - supportedFormats.Add(type.Substring(0, index)); - } + supportedFormats[i] = type.Substring(0, index); } } @@ -1839,11 +1925,13 @@ namespace Jellyfin.Api.Controllers BaseItem? item, Guid itemId, int? index, + int? width, int? height, - int? maxHeight, int? maxWidth, + int? maxHeight, + int? fillWidth, + int? fillHeight, int? quality, - int? width, bool? addPlayedIndicator, double? percentPlayed, int? unplayedCount, @@ -1851,7 +1939,6 @@ namespace Jellyfin.Api.Controllers string? backgroundColor, string? foregroundLayer, ItemImageInfo imageInfo, - bool cropWhitespace, IReadOnlyCollection<ImageFormat> supportedFormats, TimeSpan? cacheDuration, IDictionary<string, string> headers, @@ -1864,7 +1951,6 @@ namespace Jellyfin.Api.Controllers var options = new ImageProcessingOptions { - CropWhiteSpace = cropWhitespace, Height = height, ImageIndex = index ?? 0, Image = imageInfo, @@ -1872,6 +1958,8 @@ namespace Jellyfin.Api.Controllers ItemId = itemId, MaxHeight = maxHeight, MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, Quality = quality ?? 100, Width = width, AddPlayedIndicator = addPlayedIndicator ?? false, diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 244625752..f232dffaa 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; @@ -87,7 +86,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given album. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -123,7 +122,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given playlist. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -159,7 +158,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given genre. /// </summary> /// <param name="name">The genre name.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -173,7 +172,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> [HttpGet("MusicGenres/{name}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre( + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName( [FromRoute, Required] string name, [FromQuery] Guid? userId, [FromQuery] int? limit, @@ -194,7 +193,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given artist. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -230,7 +229,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given genre. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -244,7 +243,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> [HttpGet("MusicGenres/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres( + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById( [FromRoute, Required] Guid id, [FromQuery] Guid? userId, [FromQuery] int? limit, @@ -266,7 +265,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given item. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -301,6 +300,80 @@ namespace Jellyfin.Api.Controllers return GetResult(items, user, limit, dtoOptions); } + /// <summary> + /// Creates an instant playlist based on a given artist. + /// </summary> + /// <param name="id">The item id.</param> + /// <param name="userId">Optional. Filter by user id, and attach user data.</param> + /// <param name="limit">Optional. The maximum number of records to return.</param> + /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> + /// <param name="enableImages">Optional. Include image information in output.</param> + /// <param name="enableUserData">Optional. Include user data.</param> + /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> + /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> + /// <response code="200">Instant playlist returned.</response> + /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> + [HttpGet("Artists/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use GetInstantMixFromArtists")] + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2( + [FromQuery, Required] Guid id, + [FromQuery] Guid? userId, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableImages, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + { + return GetInstantMixFromArtists( + id, + userId, + limit, + fields, + enableImages, + enableUserData, + imageTypeLimit, + enableImageTypes); + } + + /// <summary> + /// Creates an instant playlist based on a given genre. + /// </summary> + /// <param name="id">The item id.</param> + /// <param name="userId">Optional. Filter by user id, and attach user data.</param> + /// <param name="limit">Optional. The maximum number of records to return.</param> + /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> + /// <param name="enableImages">Optional. Include image information in output.</param> + /// <param name="enableUserData">Optional. Include user data.</param> + /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> + /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> + /// <response code="200">Instant playlist returned.</response> + /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> + [HttpGet("MusicGenres/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use GetInstantMixFromMusicGenres instead")] + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2( + [FromQuery, Required] Guid id, + [FromQuery] Guid? userId, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableImages, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + { + return GetInstantMixFromMusicGenreById( + id, + userId, + limit, + fields, + enableImages, + enableUserData, + imageTypeLimit, + enableImageTypes); + } + private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions) { var list = items; diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 6c38f77ce..9fa307858 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -2,8 +2,6 @@ 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; @@ -240,48 +238,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Gets a remote image. - /// </summary> - /// <param name="imageUrl">The image url.</param> - /// <param name="providerName">The provider name.</param> - /// <response code="200">Remote image retrieved.</response> - /// <returns> - /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. - /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream. - /// </returns> - [HttpGet("Items/RemoteSearch/Image")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesImageFile] - public async Task<ActionResult> 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)) - { - return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath)); - } - } - 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); - var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath)); - } - - /// <summary> /// Applies search criteria to an item and refreshes metadata. /// </summary> /// <param name="itemId">Item id.</param> @@ -322,53 +278,5 @@ namespace Jellyfin.Api.Controllers return NoContent(); } - - /// <summary> - /// Downloads the image. - /// </summary> - /// <param name="providerName">Name of the provider.</param> - /// <param name="url">The URL.</param> - /// <param name="urlHash">The URL hash.</param> - /// <param name="pointerCachePath">The pointer cache path.</param> - /// <returns>Task.</returns> - private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) - { - using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); - if (result.Content.Headers.ContentType?.MediaType == null) - { - throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType)); - } - - var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1]; - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); - Directory.CreateDirectory(directory); - 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); - } - - var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); - - Directory.CreateDirectory(pointerCacheDirectory); - await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false); - } - - /// <summary> - /// Gets the full cache path. - /// </summary> - /// <param name="filename">The filename.</param> - /// <returns>System.String.</returns> - private string GetFullCachePath(string filename) - => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 9e1a39853..a9f4a5a58 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}/ContentType")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType) + public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType) { var item = _libraryManager.GetItemById(itemId); if (item == null) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 7e9035f80..74cf3b162 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; @@ -175,16 +174,16 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] bool? recursive, [FromQuery] string? searchTerm, - [FromQuery] string? sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery] string? sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -233,8 +232,8 @@ namespace Jellyfin.Api.Controllers .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (includeItemTypes.Length == 1 - && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase) - || includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase))) + && (includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.BoxSet)) { parentId = null; } @@ -247,34 +246,40 @@ namespace Jellyfin.Api.Controllers folder = _libraryManager.GetUserRootFolder(); } - if (folder is IHasCollectionType hasCollectionType - && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + string? collectionType = null; + if (folder is IHasCollectionType hasCollectionType) + { + collectionType = hasCollectionType.CollectionType; + } + + if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { recursive = true; - includeItemTypes = new[] { "Playlist" }; + includeItemTypes = new[] { BaseItemKind.Playlist }; } - bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) + var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels); + + bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1 // Assume all folders inside an EnabledChannel are enabled - || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id) + || Array.IndexOf(enabledChannels, item.Id) != -1 // Assume all items inside an EnabledChannel are enabled - || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId); + || Array.IndexOf(enabledChannels, item.ChannelId) != -1; var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) { - if (user.GetPreference(PreferenceKind.EnabledFolders).Contains( - collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), - StringComparer.OrdinalIgnoreCase)) + if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id)) { isInEnabledFolder = true; } } - if (!(item is UserRootFolder) + if (item is not UserRootFolder && !isInEnabledFolder && !user.HasPermission(PermissionKind.EnableAllFolders) - && !user.HasPermission(PermissionKind.EnableAllChannels)) + && !user.HasPermission(PermissionKind.EnableAllChannels) + && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); @@ -286,8 +291,8 @@ namespace Jellyfin.Api.Controllers { IsPlayed = isPlayed, MediaTypes = mediaTypes, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), Recursive = recursive ?? false, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), IsFavorite = isFavorite, @@ -608,16 +613,16 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] bool? recursive, [FromQuery] string? searchTerm, - [FromQuery] string? sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery] string? sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -773,8 +778,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { @@ -786,12 +791,12 @@ namespace Jellyfin.Api.Controllers var ancestorIds = Array.Empty<Guid>(); - var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes); + var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes); if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0) { ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) - .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) + .Where(i => !excludeFolderIds.Contains(i.Id)) .Select(i => i.Id) .ToArray(); } @@ -810,8 +815,8 @@ namespace Jellyfin.Api.Controllers CollapseBoxSetItems = false, EnableTotalRecordCount = enableTotalRecordCount, AncestorIds = ancestorIds, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), SearchTerm = searchTerm }); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 184843b39..4ed15e1d5 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; @@ -115,7 +114,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path)); + return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true); } /// <summary> @@ -304,7 +303,7 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <response code="204">Library scan started.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> - [HttpGet("Library/Refresh")] + [HttpPost("Library/Refresh")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> RefreshLibrary() @@ -591,17 +590,17 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Reports that new movies have been added by an external source. /// </summary> - /// <param name="updates">A list of updated media paths.</param> + /// <param name="dto">The update paths.</param> /// <response code="204">Report success.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Library/Media/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates) + public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) { - foreach (var item in updates) + foreach (var item in dto.Updates) { - _libraryMonitor.ReportFileSystemChanged(item.Path); + _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null.")); } return NoContent(); @@ -667,7 +666,7 @@ namespace Jellyfin.Api.Controllers } // TODO determine non-ASCII validity. - return PhysicalFile(path, MimeTypes.GetMimeType(path)); + return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true); } /// <summary> @@ -742,8 +741,6 @@ namespace Jellyfin.Api.Controllers { Limit = limit, IncludeItemTypes = includeItemTypes.ToArray(), - IsMovie = isMovie, - IsSeries = isSeries, SimilarTo = item, DtoOptions = dtoOptions, EnableTotalRecordCount = !isMovie ?? true, @@ -780,7 +777,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo( [FromQuery] string? libraryContentType, - [FromQuery] bool isNewLibrary) + [FromQuery] bool isNewLibrary = false) { var result = new LibraryOptionsResultDto(); diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 94995650c..be9127dd3 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> AddVirtualFolder( [FromQuery] string? name, - [FromQuery] string? collectionType, + [FromQuery] CollectionTypeOptions? collectionType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) @@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Updates a media path. /// </summary> - /// <param name="name">The name of the library.</param> - /// <param name="pathInfo">The path info.</param> + /// <param name="mediaPathRequestDto">The name of the library and path infos.</param> /// <returns>A <see cref="NoContentResult"/>.</returns> /// <response code="204">Media path updated.</response> /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> [HttpPost("Paths/Update")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaPath( - [FromQuery] string? name, - [FromBody] MediaPathInfo? pathInfo) + public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto) { - if (string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty"); } - _libraryManager.UpdateMediaPath(name, pathInfo); + _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 56d4b3933..24ee833ef 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -553,8 +553,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isSports, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery] string? sortBy, - [FromQuery] string? sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery] bool? enableImages, @@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Set channel mappings. /// </summary> - /// <param name="providerId">Provider id.</param> - /// <param name="tunerChannelId">Tuner channel id.</param> - /// <param name="providerChannelId">Provider channel id.</param> + /// <param name="setChannelMappingDto">The set channel mapping dto.</param> /// <response code="200">Created channel mapping returned.</response> /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns> [HttpPost("ChannelMappings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping( - [FromQuery] string? providerId, - [FromQuery] string? tunerChannelId, - [FromQuery] string? providerChannelId) + public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) { - return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false); + return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false); } /// <summary> diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index a76dc057a..e330f02b6 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo; 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 @@ -82,6 +83,7 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <remarks> /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. + /// Query parameters are obsolete. /// </remarks> /// <param name="itemId">The item id.</param> /// <param name="userId">The user id.</param> @@ -105,21 +107,21 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo( [FromRoute, Required] Guid itemId, - [FromQuery] Guid? userId, - [FromQuery] int? maxStreamingBitrate, - [FromQuery] long? startTimeTicks, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? mediaSourceId, - [FromQuery] string? liveStreamId, - [FromQuery] bool? autoOpenLiveStream, - [FromQuery] bool? enableDirectPlay, - [FromQuery] bool? enableDirectStream, - [FromQuery] bool? enableTranscoding, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromBody] PlaybackInfoDto? playbackInfoDto) + [FromQuery, ParameterObsolete] Guid? userId, + [FromQuery, ParameterObsolete] int? maxStreamingBitrate, + [FromQuery, ParameterObsolete] long? startTimeTicks, + [FromQuery, ParameterObsolete] int? audioStreamIndex, + [FromQuery, ParameterObsolete] int? subtitleStreamIndex, + [FromQuery, ParameterObsolete] int? maxAudioChannels, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] string? liveStreamId, + [FromQuery, ParameterObsolete] bool? autoOpenLiveStream, + [FromQuery, ParameterObsolete] bool? enableDirectPlay, + [FromQuery, ParameterObsolete] bool? enableDirectStream, + [FromQuery, ParameterObsolete] bool? enableTranscoding, + [FromQuery, ParameterObsolete] bool? allowVideoStreamCopy, + [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto) { var authInfo = _authContext.GetAuthorizationInfo(Request); @@ -258,24 +260,24 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? subtitleStreamIndex, [FromQuery] int? maxAudioChannels, [FromQuery] Guid? itemId, - [FromBody] OpenLiveStreamDto openLiveStreamDto, - [FromQuery] bool enableDirectPlay = true, - [FromQuery] bool enableDirectStream = true) + [FromBody] OpenLiveStreamDto? openLiveStreamDto, + [FromQuery] bool? enableDirectPlay, + [FromQuery] bool? enableDirectStream) { var request = new LiveStreamRequest { - OpenToken = openToken, - UserId = userId ?? Guid.Empty, - PlaySessionId = playSessionId, - MaxStreamingBitrate = maxStreamingBitrate, - StartTimeTicks = startTimeTicks, - AudioStreamIndex = audioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex, - MaxAudioChannels = maxAudioChannels, - ItemId = itemId ?? Guid.Empty, + OpenToken = openToken ?? openLiveStreamDto?.OpenToken, + UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, + PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, + MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, + StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, + AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex, + MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels, + ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty, DeviceProfile = openLiveStreamDto?.DeviceProfile, - EnableDirectPlay = enableDirectPlay, - EnableDirectStream = enableDirectStream, + EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true, + EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true, DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } }; return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 4d788ad7d..010a3b19a 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -18,6 +18,7 @@ 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 @@ -89,7 +90,7 @@ namespace Jellyfin.Api.Controllers // nameof(LiveTvProgram) }, // IsMovie = true - OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) }, Limit = 7, ParentId = parentIdGuid, Recursive = true, @@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers { IncludeItemTypes = itemTypes.ToArray(), IsMovie = true, - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, Limit = 10, IsFavoriteOrLiked = true, ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), @@ -120,7 +121,7 @@ namespace Jellyfin.Api.Controllers DtoOptions = dtoOptions }); - var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList(); + var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6)); // Get recently played directors var recentDirectors = GetDirectors(mostRecentMovies) .ToList(); @@ -191,7 +192,8 @@ namespace Jellyfin.Api.Controllers foreach (var name in names) { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + var items = _libraryManager.GetItemList( + new InternalItemsQuery(user) { Person = name, // Account for duplicates by imdb id, since the database doesn't support this yet @@ -299,9 +301,8 @@ namespace Jellyfin.Api.Controllers private IEnumerable<string> GetActors(IEnumerable<BaseItem> items) { - var people = _libraryManager.GetPeople(new InternalPeopleQuery + var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director }) { - ExcludePersonTypes = new[] { PersonType.Director }, MaxListOrder = 3 }); @@ -315,10 +316,9 @@ namespace Jellyfin.Api.Controllers private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items) { - var people = _libraryManager.GetPeople(new InternalPeopleQuery - { - PersonTypes = new[] { PersonType.Director } - }); + var people = _libraryManager.GetPeople(new InternalPeopleQuery( + new[] { PersonType.Director }, + Array.Empty<string>())); var itemIds = items.Select(i => i.Id).ToList(); diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 2608a9cd0..27eec2b9a 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -62,6 +63,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> /// <response code="200">Music genres returned.</response> @@ -74,8 +77,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -83,6 +86,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -96,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -106,7 +111,8 @@ namespace Jellyfin.Api.Controllers NameStartsWithOrGreater = nameStartsWithOrGreater, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 0ceda6815..420630cdf 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using Jellyfin.Api.Constants; @@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Sends a notification to all admins. /// </summary> - /// <param name="url">The URL of the notification.</param> - /// <param name="level">The level of the notification.</param> - /// <param name="name">The name of the notification.</param> - /// <param name="description">The description of the notification.</param> + /// <param name="notificationDto">The notification request.</param> /// <response code="204">Notification sent.</response> /// <returns>A <cref see="NoContentResult"/>.</returns> [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateAdminNotification( - [FromQuery] string? url, - [FromQuery] NotificationLevel? level, - [FromQuery] string name = "", - [FromQuery] string description = "") + public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto) { var notification = new NotificationRequest { - Name = name, - Description = description, - Url = url, - Level = level ?? NotificationLevel.Normal, + Name = notificationDto.Name, + Description = notificationDto.Description, + Url = notificationDto.Url, + Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal, UserIds = _userManager.Users .Where(user => user.HasPermission(PermissionKind.IsAdministrator)) .Select(user => user.Id) @@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers }; _notificationManager.SendNotification(notification, CancellationToken.None); - return NoContent(); } diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 6295dfc05..5dd49ef2f 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -99,7 +99,7 @@ namespace Jellyfin.Api.Controllers var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); if (!string.IsNullOrEmpty(repositoryUrl)) { - packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any()) + packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))) .ToList(); } @@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Repositories")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos) + public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos) { _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; _serverConfigurationManager.SaveConfiguration(); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 17e631197..b98307f87 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -95,10 +94,10 @@ namespace Jellyfin.Api.Controllers } var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); - var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery + var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery( + personTypes, + excludePersonTypes) { - PersonTypes = personTypes, - ExcludePersonTypes = excludePersonTypes, NameContains = searchTerm, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 3e55434c0..1667d6ede 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; using MediaBrowser.Controller.Dto; @@ -17,6 +18,7 @@ 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 { @@ -53,6 +55,14 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Creates a new playlist. /// </summary> + /// <remarks> + /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. + /// Query parameters are obsolete. + /// </remarks> + /// <param name="name">The playlist name.</param> + /// <param name="ids">The item ids.</param> + /// <param name="userId">The user id.</param> + /// <param name="mediaType">The media type.</param> /// <param name="createPlaylistRequest">The create playlist payload.</param> /// <returns> /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. @@ -61,14 +71,23 @@ namespace Jellyfin.Api.Controllers [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( - [FromBody, Required] CreatePlaylistDto createPlaylistRequest) + [FromQuery, ParameterObsolete] string? name, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids, + [FromQuery, ParameterObsolete] Guid? userId, + [FromQuery, ParameterObsolete] string? mediaType, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) { + if (ids.Count == 0) + { + ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>(); + } + var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { - Name = createPlaylistRequest.Name, - ItemIdList = createPlaylistRequest.Ids, - UserId = createPlaylistRequest.UserId, - MediaType = createPlaylistRequest.MediaType + Name = name ?? createPlaylistRequest?.Name, + ItemIdList = ids, + UserId = userId ?? createPlaylistRequest?.UserId ?? default, + MediaType = mediaType ?? createPlaylistRequest?.MediaType }).ConfigureAwait(false); return result; diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index ec7b84ff6..f256c8c25 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/Playing/Ping")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PingPlaybackSession([FromQuery] string playSessionId) + public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) { _transcodingJobHelper.PingTranscodingJob(playSessionId, null); return NoContent(); @@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? mediaSourceId, [FromQuery] int? audioStreamIndex, [FromQuery] int? subtitleStreamIndex, - [FromQuery] PlayMethod playMethod, + [FromQuery] PlayMethod? playMethod, [FromQuery] string? liveStreamId, - [FromQuery] string playSessionId, + [FromQuery] string? playSessionId, [FromQuery] bool canSeek = false) { var playbackStartInfo = new PlaybackStartInfo @@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers MediaSourceId = mediaSourceId, AudioStreamIndex = audioStreamIndex, SubtitleStreamIndex = subtitleStreamIndex, - PlayMethod = playMethod, + PlayMethod = playMethod ?? PlayMethod.Transcode, PlaySessionId = playSessionId, LiveStreamId = liveStreamId }; @@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? audioStreamIndex, [FromQuery] int? subtitleStreamIndex, [FromQuery] int? volumeLevel, - [FromQuery] PlayMethod playMethod, + [FromQuery] PlayMethod? playMethod, [FromQuery] string? liveStreamId, - [FromQuery] string playSessionId, - [FromQuery] RepeatMode repeatMode, + [FromQuery] string? playSessionId, + [FromQuery] RepeatMode? repeatMode, [FromQuery] bool isPaused = false, [FromQuery] bool isMuted = false) { @@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers AudioStreamIndex = audioStreamIndex, SubtitleStreamIndex = subtitleStreamIndex, VolumeLevel = volumeLevel, - PlayMethod = playMethod, + PlayMethod = playMethod ?? PlayMethod.Transcode, PlaySessionId = playSessionId, LiveStreamId = liveStreamId, - RepeatMode = repeatMode + RepeatMode = repeatMode ?? RepeatMode.RepeatNone }; playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); @@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers return _userDataRepository.GetUserDataDto(item, user); } - private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId) + private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId) { if (method == PlayMethod.Transcode) { diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 98f1bc2d2..7a6130719 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.PluginDtos; -using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; +using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -23,22 +26,81 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public class PluginsController : BaseJellyfinApiController { - private readonly IApplicationHost _appHost; private readonly IInstallationManager _installationManager; - - private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); + private readonly IPluginManager _pluginManager; + private readonly IConfigurationManager _config; + private readonly JsonSerializerOptions _serializerOptions; /// <summary> /// Initializes a new instance of the <see cref="PluginsController"/> class. /// </summary> - /// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param> /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> + /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param> + /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> public PluginsController( - IApplicationHost appHost, - IInstallationManager installationManager) + IInstallationManager installationManager, + IPluginManager pluginManager, + IConfigurationManager config) { - _appHost = appHost; _installationManager = installationManager; + _pluginManager = pluginManager; + _serializerOptions = JsonDefaults.Options; + _config = config; + } + + /// <summary> + /// Get plugin security info. + /// </summary> + /// <response code="200">Plugin security info returned.</response> + /// <returns>Plugin security info.</returns> + [Obsolete("This endpoint should not be used.")] + [HttpGet("SecurityInfo")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() + { + return new PluginSecurityInfo + { + IsMbSupporter = true, + SupporterKey = "IAmTotallyLegit" + }; + } + + /// <summary> + /// Gets registration status for a feature. + /// </summary> + /// <param name="name">Feature name.</param> + /// <response code="200">Registration status returned.</response> + /// <returns>Mb registration record.</returns> + [Obsolete("This endpoint should not be used.")] + [HttpPost("RegistrationRecords/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name) + { + return new MBRegistrationRecord + { + IsRegistered = true, + RegChecked = true, + TrialVersion = false, + IsValid = true, + RegError = false + }; + } + + /// <summary> + /// Gets registration status for a feature. + /// </summary> + /// <param name="name">Feature name.</param> + /// <response code="501">Not implemented.</response> + /// <returns>Not Implemented.</returns> + /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception> + [Obsolete("Paid plugins are not supported")] + [HttpGet("Registrations/{name}")] + [ProducesResponseType(StatusCodes.Status501NotImplemented)] + public static ActionResult GetRegistration([FromRoute, Required] 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(); } /// <summary> @@ -50,23 +112,74 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<PluginInfo>> GetPlugins() { - return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); + return Ok(_pluginManager.Plugins + .OrderBy(p => p.Name) + .Select(p => p.GetPluginInfo())); } /// <summary> - /// Uninstalls a plugin. + /// Enables a disabled plugin. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> + /// <response code="204">Plugin enabled.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [HttpPost("{pluginId}/{version}/Enable")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) + { + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin == null) + { + return NotFound(); + } + + _pluginManager.EnablePlugin(plugin); + return NoContent(); + } + + /// <summary> + /// Disable a plugin. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> + /// <response code="204">Plugin disabled.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [HttpPost("{pluginId}/{version}/Disable")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) + { + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin == null) + { + return NotFound(); + } + + _pluginManager.DisablePlugin(plugin); + return NoContent(); + } + + /// <summary> + /// Uninstalls a plugin by version. /// </summary> /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> /// <response code="204">Plugin uninstalled.</response> /// <response code="404">Plugin not found.</response> - /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> - [HttpDelete("{pluginId}")] + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [HttpDelete("{pluginId}/{version}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) + public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { - var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); + var plugin = _pluginManager.GetPlugin(pluginId, version); if (plugin == null) { return NotFound(); @@ -77,6 +190,35 @@ namespace Jellyfin.Api.Controllers } /// <summary> + /// Uninstalls a plugin. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <response code="204">Plugin uninstalled.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [HttpDelete("{pluginId}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Please use the UninstallPluginByVersion API.")] + public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) + { + // If no version is given, return the current instance. + var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)); + + // Select the un-instanced one first. + var plugin = plugins.FirstOrDefault(p => p.Instance == null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); + + if (plugin != null) + { + _installationManager.UninstallPlugin(plugin); + return NoContent(); + } + + return NotFound(); + } + + /// <summary> /// Gets plugin configuration. /// </summary> /// <param name="pluginId">Plugin id.</param> @@ -88,12 +230,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId) { - if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + var plugin = _pluginManager.GetPlugin(pluginId); + if (plugin?.Instance is IHasPluginConfiguration configPlugin) { - return NotFound(); + return configPlugin.Configuration; } - return plugin.Configuration; + return NotFound(); } /// <summary> @@ -105,47 +248,79 @@ namespace Jellyfin.Api.Controllers /// <param name="pluginId">Plugin id.</param> /// <response code="204">Plugin configuration updated.</response> /// <response code="404">Plugin not found or plugin does not have configuration.</response> - /// <returns> - /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration. - /// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/> - /// when plugin not found or plugin doesn't have configuration. - /// </returns> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> [HttpPost("{pluginId}/Configuration")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) { - if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + var plugin = _pluginManager.GetPlugin(pluginId); + if (plugin?.Instance is not IHasPluginConfiguration configPlugin) { return NotFound(); } - var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions) + var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions) .ConfigureAwait(false); if (configuration != null) { - plugin.UpdateConfiguration(configuration); + configPlugin.UpdateConfiguration(configuration); } return NoContent(); } /// <summary> - /// Get plugin security info. + /// Gets a plugin's image. /// </summary> - /// <response code="200">Plugin security info returned.</response> - /// <returns>Plugin security info.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpGet("SecurityInfo")] + /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> + /// <response code="200">Plugin image returned.</response> + /// <returns>Plugin's image.</returns> + [HttpGet("{pluginId}/{version}/Image")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + [AllowAnonymous] + public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { - return new PluginSecurityInfo + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin == null) { - IsMbSupporter = true, - SupporterKey = "IAmTotallyLegit" - }; + return NotFound(); + } + + var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); + if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath)) + { + return NotFound(); + } + + imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); + return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); + } + + /// <summary> + /// Gets a plugin's manifest. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <response code="204">Plugin manifest returned.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [HttpPost("{pluginId}/Manifest")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId) + { + var plugin = _pluginManager.GetPlugin(pluginId); + + if (plugin != null) + { + return plugin.Manifest; + } + + return NotFound(); } /// <summary> @@ -162,43 +337,5 @@ namespace Jellyfin.Api.Controllers { return NoContent(); } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="200">Registration status returned.</response> - /// <returns>Mb registration record.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpPost("RegistrationRecords/{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name) - { - return new MBRegistrationRecord - { - IsRegistered = true, - RegChecked = true, - TrialVersion = false, - IsValid = true, - RegError = false - }; - } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="501">Not implemented.</response> - /// <returns>Not Implemented.</returns> - /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception> - [Obsolete("Paid plugins are not supported")] - [HttpGet("Registrations/{name}")] - [ProducesResponseType(StatusCodes.Status501NotImplemented)] - public ActionResult GetRegistration([FromRoute, Required] 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/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 5284888d8..ec836f43e 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -146,58 +146,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Gets a remote image. - /// </summary> - /// <param name="imageUrl">The image url.</param> - /// <response code="200">Remote image returned.</response> - /// <response code="404">Remote image not found.</response> - /// <returns>Image Stream.</returns> - [HttpGet("Images/Remote")] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl) - { - var urlHash = imageUrl.ToString().GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string? contentPath = null; - var hasFile = false; - - try - { - contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - if (System.IO.File.Exists(contentPath)) - { - hasFile = true; - } - } - catch (FileNotFoundException) - { - // The file isn't cached yet - } - catch (IOException) - { - // 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 PhysicalFile(contentPath, contentType); - } - - /// <summary> /// Downloads a remote image for an item. /// </summary> /// <param name="itemId">Item Id.</param> @@ -259,7 +207,8 @@ namespace Jellyfin.Api.Controllers var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); Directory.CreateDirectory(fullCacheDirectory); - await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 08255ff8f..73bdf9018 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -83,8 +84,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] Guid? userId, [FromQuery, Required] string searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] Guid? parentId, [FromQuery] bool? isMovie, @@ -109,8 +110,8 @@ namespace Jellyfin.Api.Controllers IncludeStudios = includeStudios, StartIndex = startIndex, UserId = userId ?? Guid.Empty, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), MediaTypes = mediaTypes, ParentId = parentId, @@ -227,10 +228,7 @@ namespace Jellyfin.Api.Controllers itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb); } - if (itemWithImage == null) - { - itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb); - } + itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb); if (itemWithImage != null) { diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e2269a2ce..7bd0b6918 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -153,6 +153,10 @@ namespace Jellyfin.Api.Controllers /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> /// <param name="itemIds">The ids of the items to play, comma delimited.</param> /// <param name="startPositionTicks">The starting position of the first item.</param> + /// <param name="mediaSourceId">Optional. The media source id.</param> + /// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param> + /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param> + /// <param name="startIndex">Optional. The start index.</param> /// <response code="204">Instruction sent to session.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/{sessionId}/Playing")] @@ -162,13 +166,21 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string sessionId, [FromQuery, Required] PlayCommand playCommand, [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, - [FromQuery] long? startPositionTicks) + [FromQuery] long? startPositionTicks, + [FromQuery] string? mediaSourceId, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] int? startIndex) { var playRequest = new PlayRequest { ItemIds = itemIds, StartPositionTicks = startPositionTicks, - PlayCommand = playCommand + PlayCommand = playCommand, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + StartIndex = startIndex }; _sessionManager.SendPlayCommand( @@ -301,9 +313,7 @@ namespace Jellyfin.Api.Controllers /// Issues a command to a client to display a message to the user. /// </summary> /// <param name="sessionId">The session id.</param> - /// <param name="text">The message test.</param> - /// <param name="header">The message header.</param> - /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param> + /// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param> /// <response code="204">Message sent.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/{sessionId}/Message")] @@ -311,16 +321,12 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendMessageCommand( [FromRoute, Required] string sessionId, - [FromQuery, Required] string text, - [FromQuery] string? header, - [FromQuery] long? timeoutMs) + [FromBody, Required] MessageCommand command) { - var command = new MessageCommand + if (string.IsNullOrWhiteSpace(command.Header)) { - Header = string.IsNullOrEmpty(header) ? "Message from Server" : header, - TimeoutMs = timeoutMs, - Text = text - }; + command.Header = "Message from Server"; + } _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None); diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index d9cb34557..a01a617fc 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -132,7 +132,10 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.Users.First(); - user.Username = startupUserDto.Name; + if (startupUserDto.Name != null) + { + user.Username = startupUserDto.Name; + } await _userManager.UpdateUserAsync(user).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index bb54c59f6..da8f8b199 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -73,8 +74,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -96,8 +97,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index dcb8e803b..1669a659d 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets subtitles in a specified format. /// </summary> + /// <param name="routeItemId">The (route) item id.</param> + /// <param name="routeMediaSourceId">The (route) media source id.</param> + /// <param name="routeIndex">The (route) subtitle stream index.</param> + /// <param name="routeFormat">The (route) format of the returned subtitle.</param> /// <param name="itemId">The item id.</param> /// <param name="mediaSourceId">The media source id.</param> /// <param name="index">The subtitle stream index.</param> @@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> - /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param> + /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> /// <response code="200">File returned.</response> /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> - [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] + [HttpGet("Videos/{routeItemId}/routeMediaSourceId/Subtitles/{routeIndex}/Stream.{routeFormat}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public async Task<ActionResult> GetSubtitle( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string mediaSourceId, - [FromRoute, Required] int index, - [FromRoute, Required] string format, + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false, [FromQuery] long startPositionTicks = 0) { + // Set parameters to route value if not provided via query. + itemId ??= routeItemId; + mediaSourceId ??= routeMediaSourceId; + index ??= routeIndex; + format ??= routeFormat; + if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) { format = "json"; @@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers if (string.IsNullOrEmpty(format)) { - var item = (Video)_libraryManager.GetItemById(itemId); + var item = (Video)_libraryManager.GetItemById(itemId.Value); - var idString = itemId.ToString("N", CultureInfo.InvariantCulture); + var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture); var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); @@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) { - await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); using var reader = new StreamReader(stream); var text = await reader.ReadToEndAsync().ConfigureAwait(false); @@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers return File( await EncodeSubtitles( - itemId, + itemId.Value, mediaSourceId, - index, + index.Value, format, startPositionTicks, endPositionTicks, @@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets subtitles in a specified format. /// </summary> + /// <param name="routeItemId">The (route) item id.</param> + /// <param name="routeMediaSourceId">The (route) media source id.</param> + /// <param name="routeIndex">The (route) subtitle stream index.</param> + /// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param> + /// <param name="routeFormat">The (route) format of the returned subtitle.</param> /// <param name="itemId">The item id.</param> /// <param name="mediaSourceId">The media source id.</param> /// <param name="index">The subtitle stream index.</param> - /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param> + /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> /// <param name="format">The format of the returned subtitle.</param> /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> /// <response code="200">File returned.</response> /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> - [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")] + [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public Task<ActionResult> GetSubtitleWithTicks( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string mediaSourceId, - [FromRoute, Required] int index, - [FromRoute, Required] long startPositionTicks, - [FromRoute, Required] string format, + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] long routeStartPositionTicks, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] long? startPositionTicks, + [FromQuery, ParameterObsolete] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false) { return GetSubtitle( + routeItemId, + routeMediaSourceId, + routeIndex, + routeFormat, itemId, mediaSourceId, index, @@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers endPositionTicks, copyTimestamps, addVttTimeMap, - startPositionTicks); + startPositionTicks ?? routeStartPositionTicks); } /// <summary> @@ -371,6 +399,7 @@ namespace Jellyfin.Api.Controllers /// <response code="204">Subtitle uploaded.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Videos/{itemId}/Subtitles")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> UploadSubtitle( [FromRoute, Required] Guid itemId, diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 9f1dec712..a811a29c3 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -70,7 +69,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions().AddClientFields(Request); var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, MediaTypes = mediaType, IncludeItemTypes = type, IsVirtualItem = false, diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 471c9180d..f878f2329 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading; @@ -20,7 +19,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// The sync play controller. /// </summary> - [Authorize(Policy = Policies.SyncPlayAccess)] + [Authorize(Policy = Policies.SyncPlayHasAccess)] public class SyncPlayController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; @@ -51,7 +50,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("New")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayCreateGroupAccess)] + [Authorize(Policy = Policies.SyncPlayCreateGroup)] public ActionResult SyncPlayCreateGroup( [FromBody, Required] NewGroupRequestDto requestData) { @@ -69,7 +68,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Join")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayAccess)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] public ActionResult SyncPlayJoinGroup( [FromBody, Required] JoinGroupRequestDto requestData) { @@ -86,6 +85,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Leave")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayLeaveGroup() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -101,7 +101,7 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns> [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.SyncPlayAccess)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -117,6 +117,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetNewQueue")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetNewQueue( [FromBody, Required] PlayRequestDto requestData) { @@ -137,6 +138,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetPlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetPlaylistItem( [FromBody, Required] SetPlaylistItemRequestDto requestData) { @@ -154,6 +156,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("RemoveFromPlaylist")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayRemoveFromPlaylist( [FromBody, Required] RemoveFromPlaylistRequestDto requestData) { @@ -171,6 +174,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("MovePlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayMovePlaylistItem( [FromBody, Required] MovePlaylistItemRequestDto requestData) { @@ -188,6 +192,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Queue")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayQueue( [FromBody, Required] QueueRequestDto requestData) { @@ -204,6 +209,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Unpause")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayUnpause() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -219,6 +225,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Pause")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayPause() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -234,6 +241,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Stop")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayStop() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -250,6 +258,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Seek")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySeek( [FromBody, Required] SeekRequestDto requestData) { @@ -267,6 +276,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Buffering")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayBuffering( [FromBody, Required] BufferRequestDto requestData) { @@ -288,6 +298,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Ready")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayReady( [FromBody, Required] ReadyRequestDto requestData) { @@ -309,6 +320,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetIgnoreWait")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetIgnoreWait( [FromBody, Required] IgnoreWaitRequestDto requestData) { @@ -326,6 +338,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("NextItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayNextItem( [FromBody, Required] NextItemRequestDto requestData) { @@ -343,6 +356,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("PreviousItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayPreviousItem( [FromBody, Required] PreviousItemRequestDto requestData) { @@ -360,6 +374,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetRepeatMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetRepeatMode( [FromBody, Required] SetRepeatModeRequestDto requestData) { @@ -377,6 +392,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetShuffleMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetShuffleMode( [FromBody, Required] SetShuffleModeRequestDto requestData) { diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 7784e8a11..bbbe5fb8d 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; +using System.Net; using System.Net.Mime; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; @@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<SystemInfo> GetSystemInfo() { - return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); + return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// <summary> @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<PublicSystemInfo> GetPublicSystemInfo() { - return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); + return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// <summary> @@ -202,7 +202,7 @@ namespace Jellyfin.Api.Controllers // 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"); + return File(stream, "text/plain; charset=utf-8"); } /// <summary> diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs index c730ac12b..7df51c7af 100644 --- a/Jellyfin.Api/Controllers/TimeSyncController.cs +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using MediaBrowser.Model.SyncPlay; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 8e9ece14f..dd3836551 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,6 +1,7 @@ using System; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -144,15 +145,15 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] bool? recursive, [FromQuery] string? searchTerm, - [FromQuery] string? sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery] string? sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -193,7 +194,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var includeItemTypes = new[] { "Trailer" }; + var includeItemTypes = new[] { BaseItemKind.Trailer }; return _itemsController .GetItems( diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 03fd1846d..ffb726fab 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; @@ -66,7 +65,9 @@ namespace Jellyfin.Api.Controllers /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableUserData">Optional. Include user data.</param> + /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> + /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -81,7 +82,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, - [FromQuery] bool enableTotalRecordCount = true) + [FromQuery] DateTime? nextUpDateCutoff, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool disableFirstEpisode = false) { var options = new DtoOptions { Fields = fields } .AddClientFields(Request) @@ -95,7 +98,9 @@ namespace Jellyfin.Api.Controllers SeriesId = seriesId, StartIndex = startIndex, UserId = userId ?? Guid.Empty, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + DisableFirstEpisode = disableFirstEpisode, + NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue }, options); @@ -153,7 +158,7 @@ namespace Jellyfin.Api.Controllers var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { nameof(Episode) }, - OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, MinPremiereDate = minPremiereDate, StartIndex = startIndex, Limit = limit, @@ -267,7 +272,7 @@ namespace Jellyfin.Api.Controllers if (startItemId.HasValue) { episodes = episodes - .SkipWhile(i => startItemId.Value.Equals(i.Id)) + .SkipWhile(i => !startItemId.Value.Equals(i.Id)) .ToList(); } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 34c9f32fa..679f055bc 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] bool? enableRemoteMedia, - [FromQuery] bool breakOnNonKeyFrames, + [FromQuery] bool breakOnNonKeyFrames = false, [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); @@ -219,11 +219,11 @@ namespace Jellyfin.Api.Controllers AudioBitRate = audioBitRate ?? maxStreamingBitrate, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Hls, - RequireAvc = true, - DeInterlace = true, - RequireNonAnamorphic = true, - EnableMpegtsM2TsMode = true, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + RequireAvc = false, + DeInterlace = false, + RequireNonAnamorphic = false, + EnableMpegtsM2TsMode = false, + TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), Context = EncodingContext.Static, StreamOptions = new Dictionary<string, string>(), EnableAdaptiveBitrateStreaming = true @@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers CopyTimestamps = true, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Embed, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), Context = EncodingContext.Static }; @@ -278,7 +278,7 @@ namespace Jellyfin.Api.Controllers var directPlayProfiles = new DirectPlayProfile[len]; for (int i = 0; i < len; i++) { - var parts = RequestHelpers.Split(containers[i], '|', true); + var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries); var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1)); @@ -298,9 +298,9 @@ namespace Jellyfin.Api.Controllers { Type = DlnaProfileType.Audio, Context = EncodingContext.Streaming, - Container = transcodingContainer, - AudioCodec = audioCodec, - Protocol = transcodingProtocol, + Container = transcodingContainer ?? "mp3", + AudioCodec = audioCodec ?? "mp3", + Protocol = transcodingProtocol ?? "http", BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } @@ -312,25 +312,52 @@ namespace Jellyfin.Api.Controllers if (maxAudioSampleRate.HasValue) { // codec profile - conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioSampleRate, Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) }); + conditions.Add( + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + IsRequired = false, + Property = ProfileConditionValue.AudioSampleRate, + Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) + }); } if (maxAudioBitDepth.HasValue) { // codec profile - conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioBitDepth, Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) }); + conditions.Add( + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + IsRequired = false, + Property = ProfileConditionValue.AudioBitDepth, + Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) + }); } if (maxAudioChannels.HasValue) { // codec profile - conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioChannels, Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) }); + conditions.Add( + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + IsRequired = false, + Property = ProfileConditionValue.AudioChannels, + Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) + }); } if (conditions.Count > 0) { // codec profile - codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() }); + codecProfiles.Add( + new CodecProfile + { + Type = CodecType.Audio, + Container = string.Join(',', containers), + Conditions = conditions.ToArray() + }); } deviceProfile.CodecProfiles = codecProfiles.ToArray(); diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 0f0bee4bc..b13db4baa 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -21,6 +21,7 @@ using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -36,6 +37,7 @@ namespace Jellyfin.Api.Controllers private readonly IDeviceManager _deviceManager; private readonly IAuthorizationContext _authContext; private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="UserController"/> class. @@ -46,13 +48,15 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> public UserController( IUserManager userManager, ISessionManager sessionManager, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext, - IServerConfigurationManager config) + IServerConfigurationManager config, + ILogger<UserController> logger) { _userManager = userManager; _sessionManager = sessionManager; @@ -60,6 +64,7 @@ namespace Jellyfin.Api.Controllers _deviceManager = deviceManager; _authContext = authContext; _config = config; + _logger = logger; } /// <summary> @@ -118,7 +123,7 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -172,11 +177,9 @@ namespace Jellyfin.Api.Controllers return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed."); } - // Password should always be null AuthenticateUserByName request = new AuthenticateUserByName { Username = user.Username, - Password = null, Pw = pw }; return await AuthenticateUserByName(request).ConfigureAwait(false); @@ -203,8 +206,7 @@ namespace Jellyfin.Api.Controllers DeviceId = auth.DeviceId, DeviceName = auth.Device, Password = request.Pw, - PasswordSha1 = request.Password, - RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(), + RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), Username = request.Username }).ConfigureAwait(false); @@ -291,7 +293,7 @@ namespace Jellyfin.Api.Controllers user.Username, request.CurrentPw, request.CurrentPw, - HttpContext.GetNormalizedRemoteIp(), + HttpContext.GetNormalizedRemoteIp().ToString(), false).ConfigureAwait(false); if (success == null) @@ -483,7 +485,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } - var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -498,8 +500,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) { + var ip = HttpContext.GetNormalizedRemoteIp(); var isLocal = HttpContext.IsLocal() - || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()); + || _networkManager.IsInLocalNetwork(ip); + + if (isLocal) + { + _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip); + } var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); @@ -509,14 +517,14 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Redeems a forgot password pin. /// </summary> - /// <param name="pin">The pin.</param> + /// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param> /// <response code="200">Pin reset process started.</response> /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns> [HttpPost("ForgotPassword/Pin")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin) + public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest) { - var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); + var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false); return result; } @@ -581,7 +589,7 @@ namespace Jellyfin.Api.Controllers var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp())); + .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); return result; } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 0e65591cc..1d70406ac 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -269,7 +270,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, @@ -296,7 +297,7 @@ namespace Jellyfin.Api.Controllers new LatestItemsQuery { GroupItems = groupItems, - IncludeItemTypes = includeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), IsPlayed = isPlayed, Limit = limit, ParentId = parentId ?? Guid.Empty, diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index e1483ce9d..7bc5ecdf1 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 7e743ee0c..308334b23 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -12,7 +12,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -49,9 +48,6 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<VideoHlsController> _logger; @@ -61,9 +57,6 @@ namespace Jellyfin.Api.Controllers /// Initializes a new instance of the <see cref="VideoHlsController"/> class. /// </summary> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> @@ -73,11 +66,9 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param> /// <param name="logger">Instance of the <see cref="ILogger{VideoHlsController}"/>.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public VideoHlsController( IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDlnaManager dlnaManager, IUserManager userManger, IAuthorizationContext authorizationContext, @@ -86,10 +77,9 @@ namespace Jellyfin.Api.Controllers IServerConfigurationManager serverConfigurationManager, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - ILogger<VideoHlsController> logger) + ILogger<VideoHlsController> logger, + EncodingHelper encodingHelper) { - _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); - _dlnaManager = dlnaManager; _authContext = authorizationContext; _userManager = userManger; @@ -97,12 +87,11 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; + _encodingHelper = encodingHelper; + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } @@ -199,7 +188,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -214,7 +203,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -224,7 +213,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -248,28 +237,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, MaxHeight = maxHeight, MaxWidth = maxWidth, @@ -286,9 +275,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 44dc63952..e544d001e 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -49,12 +49,10 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; + private readonly EncodingHelper _encodingHelper; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; @@ -69,12 +67,10 @@ namespace Jellyfin.Api.Controllers /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public VideosController( ILibraryManager libraryManager, IUserManager userManager, @@ -84,12 +80,10 @@ namespace Jellyfin.Api.Controllers IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + EncodingHelper encodingHelper) { _libraryManager = libraryManager; _userManager = userManager; @@ -99,12 +93,10 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; + _encodingHelper = encodingHelper; } /// <summary> @@ -217,9 +209,7 @@ namespace Jellyfin.Api.Controllers return BadRequest("Please supply at least two videos to merge."); } - var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList(); - - var primaryVersion = videosWithVersions.FirstOrDefault(); + var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId)); if (primaryVersion == null) { primaryVersion = items @@ -364,7 +354,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -379,7 +369,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; @@ -388,7 +378,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -412,28 +402,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -446,9 +436,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -517,8 +505,7 @@ namespace Jellyfin.Api.Controllers // Need to start ffmpeg (because media can't be returned directly) var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast"); + var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast"); return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, @@ -540,7 +527,7 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="playSessionId">The play session id.</param> /// <param name="segmentContainer">The segment container.</param> - /// <param name="segmentLength">The segment lenght.</param> + /// <param name="segmentLength">The segment length.</param> /// <param name="minSegments">The minimum number of segments.</param> /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> @@ -569,7 +556,7 @@ namespace Jellyfin.Api.Controllers /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> - /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> + /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param> /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> /// <param name="liveStreamId">The live stream id.</param> @@ -583,8 +570,8 @@ namespace Jellyfin.Api.Controllers /// <param name="streamOptions">Optional. The streaming options.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> - [HttpGet("{itemId}/{stream=stream}.{container}")] - [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")] + [HttpGet("{itemId}/stream.{container}")] + [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesVideoFile] public Task<ActionResult> GetVideoStreamByContainer( @@ -620,7 +607,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -635,7 +622,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { return GetVideoStream( diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 48c639b08..d6dc6650c 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -70,13 +71,13 @@ namespace Jellyfin.Api.Controllers public ActionResult<QueryResult<BaseItemDto>> GetYears( [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery] string? sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery] string? sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -100,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), MediaTypes = mediaTypes, DtoOptions = dtoOptions }; @@ -192,16 +193,17 @@ namespace Jellyfin.Api.Controllers return _dtoService.GetBaseItemDto(item, dtoOptions); } - private bool FilterItem(BaseItem f, IReadOnlyCollection<string> excludeItemTypes, IReadOnlyCollection<string> includeItemTypes, IReadOnlyCollection<string> mediaTypes) + private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes) { + var baseItemKind = f.GetBaseItemKind(); // Exclude item types - if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(baseItemKind)) { return false; } // Include item types - if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind)) { return false; } |
