diff options
| author | Joshua M. Boniface <joshua@boniface.me> | 2025-08-03 17:27:17 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-03 17:27:17 -0400 |
| commit | 4b6fb6c4bb2478badad068ce18aabe0c2955db48 (patch) | |
| tree | 15f986ee62327cceb8f5c8f009bcf08d10cfaa66 /Jellyfin.Api/Controllers | |
| parent | e7bc86ebb8496615e0b3f73eb4f13ab4c0913dc8 (diff) | |
| parent | db7465e83d9cc07134a0bffad7ed17b1c7b873da (diff) | |
Merge branch 'master' into master
Diffstat (limited to 'Jellyfin.Api/Controllers')
34 files changed, 366 insertions, 142 deletions
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 10556da65..7ba75dc24 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -4,8 +4,9 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 272b4034e..e334e1264 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task<ActionResult> GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task<ActionResult> GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/BackupController.cs b/Jellyfin.Api/Controllers/BackupController.cs new file mode 100644 index 000000000..aa908ee30 --- /dev/null +++ b/Jellyfin.Api/Controllers/BackupController.cs @@ -0,0 +1,127 @@ +using System.IO; +using System.Threading.Tasks; +using Jellyfin.Server.Implementations.SystemBackupService; +using MediaBrowser.Common.Api; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.SystemBackupService; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers; + +/// <summary> +/// The backup controller. +/// </summary> +[Authorize(Policy = Policies.RequiresElevation)] +public class BackupController : BaseJellyfinApiController +{ + private readonly IBackupService _backupService; + private readonly IApplicationPaths _applicationPaths; + + /// <summary> + /// Initializes a new instance of the <see cref="BackupController"/> class. + /// </summary> + /// <param name="backupService">Instance of the <see cref="IBackupService"/> interface.</param> + /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> + public BackupController(IBackupService backupService, IApplicationPaths applicationPaths) + { + _backupService = backupService; + _applicationPaths = applicationPaths; + } + + /// <summary> + /// Creates a new Backup. + /// </summary> + /// <param name="backupOptions">The backup options.</param> + /// <response code="200">Backup created.</response> + /// <response code="403">User does not have permission to retrieve information.</response> + /// <returns>The created backup manifest.</returns> + [HttpPost("Create")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task<ActionResult<BackupManifestDto>> CreateBackup([FromBody] BackupOptionsDto backupOptions) + { + return Ok(await _backupService.CreateBackupAsync(backupOptions ?? new()).ConfigureAwait(false)); + } + + /// <summary> + /// Restores to a backup by restarting the server and applying the backup. + /// </summary> + /// <param name="archiveRestoreDto">The data to start a restore process.</param> + /// <response code="204">Backup restore started.</response> + /// <response code="403">User does not have permission to retrieve information.</response> + /// <returns>No-Content.</returns> + [HttpPost("Restore")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public IActionResult StartRestoreBackup([FromBody, BindRequired] BackupRestoreRequestDto archiveRestoreDto) + { + var archivePath = SanitizePath(archiveRestoreDto.ArchiveFileName); + if (!System.IO.File.Exists(archivePath)) + { + return NotFound(); + } + + _backupService.ScheduleRestoreAndRestartServer(archivePath); + return NoContent(); + } + + /// <summary> + /// Gets a list of all currently present backups in the backup directory. + /// </summary> + /// <response code="200">Backups available.</response> + /// <response code="403">User does not have permission to retrieve information.</response> + /// <returns>The list of backups.</returns> + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task<ActionResult<BackupManifestDto[]>> ListBackups() + { + return Ok(await _backupService.EnumerateBackups().ConfigureAwait(false)); + } + + /// <summary> + /// Gets the descriptor from an existing archive is present. + /// </summary> + /// <param name="path">The data to start a restore process.</param> + /// <response code="200">Backup archive manifest.</response> + /// <response code="204">Not a valid jellyfin Archive.</response> + /// <response code="404">Not a valid path.</response> + /// <response code="403">User does not have permission to retrieve information.</response> + /// <returns>The backup manifest.</returns> + [HttpGet("Manifest")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task<ActionResult<BackupManifestDto>> GetBackup([BindRequired] string path) + { + var backupPath = SanitizePath(path); + + if (!System.IO.File.Exists(backupPath)) + { + return NotFound(); + } + + var manifest = await _backupService.GetBackupManifest(backupPath).ConfigureAwait(false); + if (manifest is null) + { + return NoContent(); + } + + return Ok(manifest); + } + + [NonAction] + private string SanitizePath(string path) + { + // sanitize path + var archiveRestorePath = Path.GetFileName(Path.GetFullPath(path)); + var archivePath = Path.Combine(_applicationPaths.BackupPath, archiveRestorePath); + return archivePath; + } +} diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs index 3c2c4b4db..1d948ff20 100644 --- a/Jellyfin.Api/Controllers/BrandingController.cs +++ b/Jellyfin.Api/Controllers/BrandingController.cs @@ -29,9 +29,18 @@ public class BrandingController : BaseJellyfinApiController /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns> [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<BrandingOptions> GetBrandingOptions() + public ActionResult<BrandingOptionsDto> GetBrandingOptions() { - return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + + var brandingOptionsDto = new BrandingOptionsDto + { + LoginDisclaimer = brandingOptions.LoginDisclaimer, + CustomCss = brandingOptions.CustomCss, + SplashscreenEnabled = brandingOptions.SplashscreenEnabled + }; + + return brandingOptionsDto; } /// <summary> diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 2f55e88ec..880b3a82d 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index abe8bec2d..8dcaebf6d 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -9,6 +9,7 @@ using Jellyfin.Extensions.Json; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Branding; using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -120,6 +121,30 @@ public class ConfigurationController : BaseJellyfinApiController } /// <summary> + /// Updates branding configuration. + /// </summary> + /// <param name="configuration">Branding configuration.</param> + /// <response code="204">Branding configuration updated.</response> + /// <returns>Update status.</returns> + [HttpPost("Configuration/Branding")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateBrandingConfiguration([FromBody, Required] BrandingOptionsDto configuration) + { + // Get the current branding configuration to preserve SplashscreenLocation + var currentBranding = (BrandingOptions)_configurationManager.GetConfiguration("branding"); + + // Update only the properties from BrandingOptionsDto + currentBranding.LoginDisclaimer = configuration.LoginDisclaimer; + currentBranding.CustomCss = configuration.CustomCss; + currentBranding.SplashscreenEnabled = configuration.SplashscreenEnabled; + + _configurationManager.SaveConfiguration("branding", currentBranding); + + return NoContent(); + } + + /// <summary> /// Updates the path to the media encoder. /// </summary> /// <param name="mediaEncoderPath">Media encoder path form body.</param> diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 6d94d96f3..13064882c 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -4,8 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using Jellyfin.Api.Helpers; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Model.Dto; diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index ca8ab0ef7..2614fe995 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -46,6 +46,7 @@ public class DynamicHlsController : BaseJellyfinApiController private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0); private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1); + private readonly Version _minFFmpegHlsSegmentOptions = new Version(5, 0); private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; @@ -166,18 +167,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task<ActionResult> GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -188,7 +189,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -207,8 +208,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -415,12 +416,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -431,7 +432,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -452,8 +453,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -591,12 +592,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -608,7 +609,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -627,8 +628,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -761,12 +762,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -777,7 +778,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -798,8 +799,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -933,12 +934,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -950,7 +951,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -969,8 +970,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1106,7 +1107,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1114,12 +1115,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1130,7 +1131,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1151,8 +1152,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1291,7 +1292,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1299,12 +1300,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1316,7 +1317,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1335,8 +1336,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1419,8 +1420,9 @@ public class DynamicHlsController : BaseJellyfinApiController TranscodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); - + var mediaSourceId = state.BaseRequest.MediaSourceId; var request = new CreateMainPlaylistRequest( + mediaSourceId is null ? null : Guid.Parse(mediaSourceId), state.MediaPath, state.SegmentLength * 1000, state.RunTimeTicks ?? 0, @@ -1605,6 +1607,7 @@ public class DynamicHlsController : BaseJellyfinApiController var segmentFormat = string.Empty; var segmentContainer = outputExtension.TrimStart('.'); var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer); + var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0"; if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) { @@ -1620,6 +1623,11 @@ public class DynamicHlsController : BaseJellyfinApiController false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" }; + var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions; + + // fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT + hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont"; + segmentFormat = "fmp4" + outputFmp4HeaderArg; } else @@ -1641,8 +1649,6 @@ public class DynamicHlsController : BaseJellyfinApiController Path.GetFileNameWithoutExtension(outputPath)); } - var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0"; - return string.Format( CultureInfo.InvariantCulture, "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"", @@ -1675,7 +1681,7 @@ public class DynamicHlsController : BaseJellyfinApiController } var audioCodec = _encodingHelper.GetAudioEncoder(state); - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); + var bitStreamArgs = _encodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer var strictArgs = string.Empty; @@ -1753,7 +1759,7 @@ public class DynamicHlsController : BaseJellyfinApiController if (channels.HasValue && (channels.Value != 2 - || (state.AudioStream?.Channels != null && !useDownMixAlgorithm))) + || (state.AudioStream?.Channels is not null && !useDownMixAlgorithm))) { args += " -ac " + channels.Value; } @@ -1822,10 +1828,11 @@ public class DynamicHlsController : BaseJellyfinApiController // Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer. // Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks. var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); - var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR; + var videoIsDoVi = EncodingHelper.IsDovi(state.VideoStream); if (EncodingHelper.IsCopyCodec(codec) - && (videoIsDoVi && clientSupportsDoVi)) + && (videoIsDoVi && clientSupportsDoVi) + && !_encodingHelper.IsDoviRemoved(state)) { if (isActualOutputVideoCodecHevc) { @@ -1855,7 +1862,7 @@ public class DynamicHlsController : BaseJellyfinApiController // If h264_mp4toannexb is ever added, do not use it for live tv. if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); + string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state, MediaStreamType.Video); if (!string.IsNullOrEmpty(bitStreamArgs)) { args += " " + bitStreamArgs; diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index f0d17decb..dd60d01e0 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -4,8 +4,9 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index b71199026..abda053d3 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -130,7 +130,7 @@ public class ImageController : BaseJellyfinApiController await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); + user.ProfileImage = new Database.Implementations.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager .SaveImage(stream, mimeType, user.ProfileImage.Path) @@ -1727,7 +1727,8 @@ public class ImageController : BaseJellyfinApiController [FromQuery, Range(0, 100)] int quality = 90) { var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); - if (!brandingOptions.SplashscreenEnabled) + var isAdmin = User.IsInRole(Constants.UserRoles.Administrator); + if (!brandingOptions.SplashscreenEnabled && !isAdmin) { return NotFound(); } diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index e326b925b..c4b976756 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -6,7 +6,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index d49e0753e..50eeaeac6 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -299,7 +299,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!season.LockedFields.Contains(MetadataField.Tags)) { - season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } season.OnMetadataChanged(); @@ -316,7 +316,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!ep.LockedFields.Contains(MetadataField.Tags)) { - ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } ep.OnMetadataChanged(); @@ -337,7 +337,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!ep.LockedFields.Contains(MetadataField.Tags)) { - ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } ep.OnMetadataChanged(); @@ -357,7 +357,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!track.LockedFields.Contains(MetadataField.Tags)) { - track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } track.OnMetadataChanged(); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index ed2f49b86..a49128336 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -4,7 +4,9 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; @@ -446,13 +448,13 @@ public class ItemsController : BaseJellyfinApiController // Min official rating if (!string.IsNullOrWhiteSpace(minOfficialRating)) { - query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating); + query.MinParentalRating = _localization.GetRatingScore(minOfficialRating); } // Max official rating if (!string.IsNullOrWhiteSpace(maxOfficialRating)) { - query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating); + query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating); } // Artists diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 7c6160fc4..bde1758e9 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -11,8 +11,9 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index a3b4c8700..10f1789ad 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; @@ -698,6 +699,7 @@ public class LiveTvController : BaseJellyfinApiController /// Gets recommended live tv epgs. /// </summary> /// <param name="userId">Optional. filter by user id.</param> + /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param> /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param> @@ -720,6 +722,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms( [FromQuery] Guid? userId, + [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? isAiring, [FromQuery] bool? hasAired, @@ -744,6 +747,7 @@ public class LiveTvController : BaseJellyfinApiController var query = new InternalItemsQuery(user) { IsAiring = isAiring, + StartIndex = startIndex, Limit = limit, HasAired = hasAired, IsSeries = isSeries, @@ -1186,7 +1190,9 @@ public class LiveTvController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesVideoFile] - public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container) + public ActionResult GetLiveStreamFile( + [FromRoute, Required] string streamId, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index f65d95c41..bbce5a9e1 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Jellyfin.Api.Constants; using MediaBrowser.Common.Api; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -45,7 +44,7 @@ public class LocalizationController : BaseJellyfinApiController /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns> [HttpGet("Countries")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<CountryInfo>> GetCountries() + public ActionResult<IReadOnlyList<CountryInfo>> GetCountries() { return Ok(_localization.GetCountries()); } @@ -57,7 +56,7 @@ public class LocalizationController : BaseJellyfinApiController /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns> [HttpGet("ParentalRatings")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings() + public ActionResult<IReadOnlyList<ParentalRating>> GetParentalRatings() { return Ok(_localization.GetParentalRatings()); } diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index 2d1d4e2c8..b8836d7cf 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -4,10 +4,10 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Extensions; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Model.MediaSegments; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; @@ -55,7 +55,8 @@ public class MediaSegmentsController : BaseJellyfinApiController return NotFound(); } - var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false); + var libraryOptions = _libraryManager.GetLibraryOptions(item); + var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes, libraryOptions).ConfigureAwait(false); return Ok(new QueryResult<MediaSegmentDto>(items.ToArray())); } } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index cbbaaddbf..363acf815 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -5,8 +5,9 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index e8bc8f265..1e45e53ca 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -4,8 +4,9 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index b0c493fbe..4d12dc18f 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -4,7 +4,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index ec5fdab38..79c71d23a 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -450,22 +450,41 @@ public class PlaylistsController : BaseJellyfinApiController { var callingUserId = User.GetUserId(); - var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId); - if (playlist is null) + if (!callingUserId.IsEmpty()) { - return NotFound("Playlist not found"); + var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Forbid(); + } } + else + { + var isApiKey = User.GetIsApiKey(); - var isPermitted = playlist.OwnerUserId.Equals(callingUserId) - || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + if (!isApiKey) + { + return Forbid(); + } + } - if (!isPermitted) + try { - return Forbid(); + await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); + return NoContent(); + } + catch (ArgumentException) + { + return NotFound(); } - - await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); - return NoContent(); } /// <summary> diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 794c6500c..ade0906b3 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -272,6 +272,7 @@ public class PlaystateController : BaseJellyfinApiController /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("PlayingItems/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("This endpoint is obsolete. Use ReportPlaybackStart instead")] public async Task<ActionResult> OnPlaybackStart( [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, @@ -350,6 +351,7 @@ public class PlaystateController : BaseJellyfinApiController /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("PlayingItems/{itemId}/Progress")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("This endpoint is obsolete. Use ReportPlaybackProgress instead")] public async Task<ActionResult> OnPlaybackProgress( [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, @@ -438,6 +440,7 @@ public class PlaystateController : BaseJellyfinApiController /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpDelete("PlayingItems/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("This endpoint is obsolete. Use ReportPlaybackStop instead")] public async Task<ActionResult> OnPlaybackStopped( [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index b3b900ceb..3bb68553d 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -59,6 +59,7 @@ public class StartupController : BaseJellyfinApiController { return new StartupConfigurationDto { + ServerName = _config.Configuration.ServerName, UICulture = _config.Configuration.UICulture, MetadataCountryCode = _config.Configuration.MetadataCountryCode, PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage @@ -75,6 +76,7 @@ public class StartupController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) { + _config.Configuration.ServerName = startupConfiguration.ServerName ?? string.Empty; _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty; _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty; _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty; @@ -113,8 +115,7 @@ public class StartupController : BaseJellyfinApiController var user = _userManager.Users.First(); return new StartupUserDto { - Name = user.Username, - Password = user.Password + Name = user.Username }; } diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 43c5384dc..52cb87e72 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -3,8 +3,8 @@ using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 9b56d0849..52982c362 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -3,8 +3,9 @@ using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 383978197..3d6874079 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -1,9 +1,9 @@ +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.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; using MediaBrowser.Common.Api; @@ -50,17 +50,16 @@ public class SyncPlayController : BaseJellyfinApiController /// </summary> /// <param name="requestData">The settings of the new group.</param> /// <response code="204">New group created.</response> - /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + /// <returns>An <see cref="GroupInfoDto"/> for the created group.</returns> [HttpPost("New")] - [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.SyncPlayCreateGroup)] - public async Task<ActionResult> SyncPlayCreateGroup( + public async Task<ActionResult<GroupInfoDto>> SyncPlayCreateGroup( [FromBody, Required] NewGroupRequestDto requestData) { var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new NewGroupRequest(requestData.GroupName); - _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); + return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None)); } /// <summary> @@ -113,6 +112,23 @@ public class SyncPlayController : BaseJellyfinApiController } /// <summary> + /// Gets a SyncPlay group by id. + /// </summary> + /// <param name="id">The id of the group.</param> + /// <response code="200">Group returned.</response> + /// <returns>An <see cref="GroupInfoDto"/> for the requested group.</returns> + [HttpGet("{id:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] + public async Task<ActionResult<GroupInfoDto>> SyncPlayGetGroup([FromRoute] Guid id) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var group = _syncPlayManager.GetGroup(currentSession, id); + return group is null ? NotFound() : Ok(group); + } + + /// <summary> /// Request to set new playlist in SyncPlay group. /// </summary> /// <param name="requestData">The new playlist to play in the group.</param> diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 6c5ce4715..450225c37 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -5,7 +5,7 @@ using System.IO; using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.SystemInfoDtos; using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -72,6 +72,19 @@ public class SystemController : BaseJellyfinApiController => _systemManager.GetSystemInfo(Request); /// <summary> + /// Gets information about the server. + /// </summary> + /// <response code="200">Information retrieved.</response> + /// <response code="403">User does not have permission to retrieve information.</response> + /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns> + [HttpGet("Info/Storage")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult<SystemStorageDto> GetSystemStorage() + => Ok(SystemStorageDto.FromSystemStorageInfo(_systemManager.GetSystemStorageInfo())); + + /// <summary> /// Gets public information about the server. /// </summary> /// <response code="200">Information retrieved.</response> @@ -212,20 +225,4 @@ public class SystemController : BaseJellyfinApiController FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); return File(stream, "text/plain; charset=utf-8"); } - - /// <summary> - /// Gets wake on lan information. - /// </summary> - /// <response code="200">Information retrieved.</response> - /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns> - [HttpGet("WakeOnLanInfo")] - [Authorize] - [Obsolete("This endpoint is obsolete.")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() - { - var result = _networkManager.GetMacAddresses() - .Select(i => new WakeOnLanInfo(i)); - return Ok(result); - } } diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 7ee4396bb..3e4bac89a 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,6 +1,7 @@ using System; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index df46c2dac..0f08854d2 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -86,7 +88,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool disableFirstEpisode = false, + [FromQuery][ParameterObsolete] bool disableFirstEpisode = false, [FromQuery] bool enableResumable = true, [FromQuery] bool enableRewatching = false) { @@ -109,7 +111,6 @@ public class TvShowsController : BaseJellyfinApiController StartIndex = startIndex, User = user, EnableTotalRecordCount = enableTotalRecordCount, - DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, EnableResumable = enableResumable, EnableRewatching = enableRewatching diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index a5b5fde62..fd6334703 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index d7886d247..d0ced277a 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -7,7 +7,8 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; -using Jellyfin.Data.Enums; +using Jellyfin.Data; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 6cc2b4244..0e04beb14 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -7,8 +7,8 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 6f18c1603..97f3239bb 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task<ActionResult> GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task<ActionResult> GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 2b32ae728..b60258586 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -6,8 +6,9 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -222,6 +223,6 @@ public class YearsController : BaseJellyfinApiController .Select(i => i.ProductionYear ?? 0) .Where(i => i > 0) .Distinct() - .Select(year => _libraryManager.GetYear(year)); + .Select(_libraryManager.GetYear); } } |
