aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/automation.yml10
-rw-r--r--.github/workflows/codeql-analysis.yml10
-rw-r--r--.github/workflows/commands.yml14
-rw-r--r--.github/workflows/openapi.yml22
-rw-r--r--.github/workflows/repo-stale.yaml3
-rw-r--r--.npmrc3
-rw-r--r--CONTRIBUTORS.md3
-rw-r--r--Directory.Packages.props90
-rw-r--r--Dockerfile3
-rw-r--r--Dockerfile.arm1
-rw-r--r--Dockerfile.arm641
-rw-r--r--DvdLib/BigEndianBinaryReader.cs25
-rw-r--r--DvdLib/DvdLib.csproj20
-rw-r--r--DvdLib/Ifo/Cell.cs23
-rw-r--r--DvdLib/Ifo/CellPlaybackInfo.cs52
-rw-r--r--DvdLib/Ifo/CellPositionInfo.cs19
-rw-r--r--DvdLib/Ifo/Chapter.cs20
-rw-r--r--DvdLib/Ifo/Dvd.cs167
-rw-r--r--DvdLib/Ifo/DvdTime.cs39
-rw-r--r--DvdLib/Ifo/Program.cs16
-rw-r--r--DvdLib/Ifo/ProgramChain.cs121
-rw-r--r--DvdLib/Ifo/Title.cs70
-rw-r--r--DvdLib/Ifo/UserOperation.cs37
-rw-r--r--DvdLib/Properties/AssemblyInfo.cs21
-rw-r--r--Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs4
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs4
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj10
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs3
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs90
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs5
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs26
-rw-r--r--Emby.Naming/Audio/AlbumParser.cs9
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParser.cs6
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs20
-rw-r--r--Emby.Naming/AudioBook/AudioBookNameParser.cs4
-rw-r--r--Emby.Naming/Common/NamingOptions.cs42
-rw-r--r--Emby.Naming/Emby.Naming.csproj10
-rw-r--r--Emby.Naming/TV/EpisodePathParser.cs14
-rw-r--r--Emby.Naming/TV/SeriesResolver.cs2
-rw-r--r--Emby.Naming/Video/CleanDateTimeParser.cs2
-rw-r--r--Emby.Naming/Video/ExtraRuleResolver.cs2
-rw-r--r--Emby.Naming/Video/FileStackRule.cs2
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs56
-rw-r--r--Emby.Notifications/CoreNotificationTypes.cs123
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj35
-rw-r--r--Emby.Notifications/NotificationConfigurationFactory.cs23
-rw-r--r--Emby.Notifications/NotificationEntryPoint.cs314
-rw-r--r--Emby.Notifications/NotificationManager.cs224
-rw-r--r--Emby.Notifications/Properties/AssemblyInfo.cs21
-rw-r--r--Emby.Photos/Emby.Photos.csproj10
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs52
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs114
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs39
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs18
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs305
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs19
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj31
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs18
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs13
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs10
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs41
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs15
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs23
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs22
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs18
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs23
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs32
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs46
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json127
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json13
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json12
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs144
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/0-prefer.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.csv20
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/be.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.csv14
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.csv26
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/co.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/de.csv22
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/dk.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.csv30
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fi.csv20
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fr.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.csv29
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/jp.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/kz.csv13
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/mx.csv12
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nl.csv14
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.csv26
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ro.csv7
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ru.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/se.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/uk.csv29
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.csv73
-rw-r--r--Emby.Server.Implementations/Plugins/PluginLoadContext.cs33
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs63
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs13
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs4
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs13
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs5
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs7
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs5
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs5
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs12
-rw-r--r--Jellyfin.Api/Attributes/AcceptsFileAttribute.cs39
-rw-r--r--Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs23
-rw-r--r--Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs39
-rw-r--r--Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs39
-rw-r--r--Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs15
-rw-r--r--Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs23
-rw-r--r--Jellyfin.Api/Attributes/ProducesFileAttribute.cs39
-rw-r--r--Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs23
-rw-r--r--Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs23
-rw-r--r--Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs23
-rw-r--r--Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs3
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs113
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs60
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs13
-rw-r--r--Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs44
-rw-r--r--Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs56
-rw-r--r--Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs56
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs (renamed from Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs)46
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs25
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs44
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs27
-rw-r--r--Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs2
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs44
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs45
-rw-r--r--Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs46
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs6
-rw-r--r--Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs26
-rw-r--r--Jellyfin.Api/BaseJellyfinApiController.cs53
-rw-r--r--Jellyfin.Api/Constants/AuthenticationSchemes.cs17
-rw-r--r--Jellyfin.Api/Constants/InternalClaimTypes.cs65
-rw-r--r--Jellyfin.Api/Constants/Policies.cs159
-rw-r--r--Jellyfin.Api/Constants/UserRoles.cs33
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs77
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs109
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs813
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs679
-rw-r--r--Jellyfin.Api/Controllers/BrandingController.cs85
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs419
-rw-r--r--Jellyfin.Api/Controllers/ClientLogController.cs109
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs167
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs209
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs158
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs211
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs342
-rw-r--r--Jellyfin.Api/Controllers/DlnaController.cs207
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs551
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs3663
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs285
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs362
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs325
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs296
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs3819
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs654
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs464
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs125
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs699
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs1599
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs1547
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs509
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs2190
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs115
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs525
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs472
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs309
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs53
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs269
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs215
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs352
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs635
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs412
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs199
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs277
-rw-r--r--Jellyfin.Api/Controllers/ScheduledTasksController.cs243
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs405
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs833
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs237
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs238
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs887
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs132
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs757
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs355
-rw-r--r--Jellyfin.Api/Controllers/TimeSyncController.cs41
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs559
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs639
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs524
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs966
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs867
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs196
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs113
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs1176
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs334
-rw-r--r--Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs3
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs172
-rw-r--r--Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs20
-rw-r--r--Jellyfin.Api/Formatters/CssOutputFormatter.cs35
-rw-r--r--Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs23
-rw-r--r--Jellyfin.Api/Formatters/XmlOutputFormatter.cs32
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs273
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs1161
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs171
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs307
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs193
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs773
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs261
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs237
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs1233
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs1408
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj17
-rw-r--r--Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs80
-rw-r--r--Jellyfin.Api/Middleware/ExceptionMiddleware.cs150
-rw-r--r--Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs49
-rw-r--r--Jellyfin.Api/Middleware/LanFilteringMiddleware.cs49
-rw-r--r--Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs53
-rw-r--r--Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs38
-rw-r--r--Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs68
-rw-r--r--Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs46
-rw-r--r--Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs50
-rw-r--r--Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs83
-rw-r--r--Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs39
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs113
-rw-r--r--Jellyfin.Api/ModelBinders/LegacyDateTimeModelBinder.cs61
-rw-r--r--Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs61
-rw-r--r--Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs31
-rw-r--r--Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs113
-rw-r--r--Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs31
-rw-r--r--Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs25
-rw-r--r--Jellyfin.Api/Models/ConfigurationPageInfo.cs101
-rw-r--r--Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs17
-rw-r--r--Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs33
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs25
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs41
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs49
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs17
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs27
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs17
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs35
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs25
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs29
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs42
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs335
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs39
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs113
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs157
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs481
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs305
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs43
-rw-r--r--Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs129
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs33
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs29
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs25
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs17
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs17
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamState.cs301
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs99
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs27
-rw-r--r--Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs49
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/BufferRequestDto.cs61
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/IgnoreWaitRequestDto.cs19
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/JoinGroupRequestDto.cs19
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/MovePlaylistItemRequestDto.cs41
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs31
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/NextItemRequestDto.cs31
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/PingRequestDto.cs19
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/PlayRequestDto.cs51
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/PreviousItemRequestDto.cs31
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/QueueRequestDto.cs41
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/ReadyRequestDto.cs61
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs51
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/SeekRequestDto.cs19
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/SetPlaylistItemRequestDto.cs31
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/SetRepeatModeRequestDto.cs19
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/SetShuffleModeRequestDto.cs19
-rw-r--r--Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs25
-rw-r--r--Jellyfin.Api/Models/UserDtos/CreateUserByName.cs28
-rw-r--r--Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs19
-rw-r--r--Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs19
-rw-r--r--Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs19
-rw-r--r--Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs33
-rw-r--r--Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs41
-rw-r--r--Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs25
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs85
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs117
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs179
-rw-r--r--Jellyfin.Data/DayOfWeekHelper.cs11
-rw-r--r--Jellyfin.Data/Entities/User.cs4
-rw-r--r--Jellyfin.Data/Enums/PermissionKind.cs7
-rw-r--r--Jellyfin.Data/Enums/PreferenceKind.cs7
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj12
-rw-r--r--Jellyfin.Networking/Jellyfin.Networking.csproj8
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs10
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs32
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs35
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs7
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj20
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs162
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs188
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs10
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs2
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthenticationManager.cs6
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs4
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs7
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs16
-rw-r--r--Jellyfin.Server/CoreAppHost.cs2
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs2
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs149
-rw-r--r--Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs90
-rw-r--r--Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs67
-rw-r--r--Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs21
-rw-r--r--Jellyfin.Server/Formatters/CssOutputFormatter.cs36
-rw-r--r--Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs24
-rw-r--r--Jellyfin.Server/Formatters/XmlOutputFormatter.cs33
-rw-r--r--Jellyfin.Server/Helpers/StartupHelpers.cs303
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj38
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs81
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs151
-rw-r--r--Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs50
-rw-r--r--Jellyfin.Server/Middleware/LanFilteringMiddleware.cs45
-rw-r--r--Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs54
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs39
-rw-r--r--Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs69
-rw-r--r--Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs47
-rw-r--r--Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs51
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs84
-rw-r--r--Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs40
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs4
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs89
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs86
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs6
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs5
-rw-r--r--Jellyfin.Server/Program.cs467
-rw-r--r--Jellyfin.Server/Startup.cs7
-rw-r--r--Jellyfin.Server/StartupOptions.cs8
-rw-r--r--Jellyfin.sln12
-rw-r--r--Jellyfin.sln.DotSettings3
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs6
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj14
-rw-r--r--MediaBrowser.Common/Net/CustomHeaderNames.cs13
-rw-r--r--MediaBrowser.Common/Net/IPHost.cs4
-rw-r--r--MediaBrowser.Common/Net/IPNetAddress.cs5
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs5
-rw-r--r--MediaBrowser.Common/System/OperatingSystem.cs74
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs10
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs3
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs7
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs57
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs5
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs10
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs25
-rw-r--r--MediaBrowser.Controller/Library/LibraryManagerExtensions.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs12
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj16
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs451
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs15
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs24
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs8
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs5
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationManager.cs43
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationService.cs34
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs16
-rw-r--r--MediaBrowser.Controller/Notifications/UserNotification.cs25
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs2
-rw-r--r--MediaBrowser.Controller/Providers/EpisodeInfo.cs3
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs1
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs8
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj8
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs5
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs160
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs283
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs81
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs12
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs29
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs195
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj18
-rw-r--r--MediaBrowser.MediaEncoding/Probing/CodecType.cs32
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs166
-rw-r--r--MediaBrowser.Model/Branding/BrandingOptions.cs2
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs3
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs16
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs39
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs2
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs6
-rw-r--r--MediaBrowser.Model/Dlna/ITranscoderSupport.cs18
-rw-r--r--MediaBrowser.Model/Dlna/MediaOptions.cs10
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs2
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs187
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs29
-rw-r--r--MediaBrowser.Model/Dto/ImageOptions.cs111
-rw-r--r--MediaBrowser.Model/Entities/ParentalRating.cs4
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs3
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj18
-rw-r--r--MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs56
-rw-r--r--MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs21
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs5
-rw-r--r--MediaBrowser.Model/Notifications/NotificationLevel.cs11
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOption.cs55
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOptions.cs131
-rw-r--r--MediaBrowser.Model/Notifications/NotificationRequest.cs35
-rw-r--r--MediaBrowser.Model/Notifications/NotificationTypeInfo.cs18
-rw-r--r--MediaBrowser.Model/Notifications/SendToUserType.cs11
-rw-r--r--MediaBrowser.Model/System/OperatingSystemId.cs12
-rw-r--r--MediaBrowser.Model/System/PublicSystemInfo.cs5
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs11
-rw-r--r--MediaBrowser.Model/Tasks/ITaskManager.cs4
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs11
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs9
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs45
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj27
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs5
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs225
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs6
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html5
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html15
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs90
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs55
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html9
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html3
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs53
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs52
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs4
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs78
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj8
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs124
-rwxr-xr-xdebian/bin/restart.sh56
-rw-r--r--debian/conf/jellyfin5
-rw-r--r--debian/conf/jellyfin-sudoers33
-rw-r--r--debian/control5
-rw-r--r--debian/install2
-rw-r--r--debian/jellyfin.service2
-rw-r--r--debian/postinst2
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--fedora/jellyfin.env3
-rw-r--r--fedora/jellyfin.service2
-rw-r--r--fedora/jellyfin.spec20
-rw-r--r--fedora/jellyfin.sudoers14
-rwxr-xr-xfedora/restart.sh56
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj8
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj2
-rw-r--r--nuget.config6
-rw-r--r--src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj18
-rw-r--r--src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs47
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs8
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs40
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj8
-rw-r--r--src/Jellyfin.Extensions/AlphanumericComparator.cs43
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj12
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs34
-rw-r--r--src/Jellyfin.Extensions/Json/JsonDefaults.cs1
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs2
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj12
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj14
-rw-r--r--tests/Directory.Build.props23
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs32
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs (renamed from tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs)19
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs8
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs59
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs53
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs36
-rw-r--r--tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs80
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj37
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj27
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj27
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj27
-rw-r--r--tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs47
-rw-r--r--tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs5
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj27
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs37
-rw-r--r--tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj24
-rw-r--r--tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj26
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs4
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs24
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs25
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj33
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs125
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json81
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json133
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json72
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json (renamed from tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json)0
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs14
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj29
-rw-r--r--tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj27
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs5
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs55
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj29
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs1
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj27
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj34
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs76
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs10
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs19
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs25
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml6
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs28
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs64
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs40
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs26
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs61
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs27
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs25
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs129
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs27
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj38
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs9
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj37
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj27
597 files changed, 29316 insertions, 29995 deletions
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index 2dc7fb5a3..4b5571c77 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -27,7 +27,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
+ uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -36,7 +36,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
+ uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
@@ -45,7 +45,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
+ uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -59,7 +59,7 @@ jobs:
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
+ uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
@@ -68,7 +68,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
+ uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 238e31f4b..cdb7970ac 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@515828d97454b8354517688ddc5b48402b723750 # v2
+ uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@515828d97454b8354517688ddc5b48402b723750 # v2
+ uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@515828d97454b8354517688ddc5b48402b723750 # v2
+ uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 5d945c001..fe1317a75 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index 4577ff525..8fa8971ec 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,18 +14,18 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-head
retention-days: 14
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -51,13 +51,13 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-base
retention-days: 14
@@ -76,12 +76,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-base
path: openapi-base
@@ -103,14 +103,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2
+ uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2.3.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -125,7 +125,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 897b7014a..276bb2150 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -12,13 +12,14 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
+ - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
+ operations-per-run: 75
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index b7a317000..000000000
--- a/.npmrc
+++ /dev/null
@@ -1,3 +0,0 @@
-registry=https://registry.npmjs.org/
-@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
-always-auth=true \ No newline at end of file
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ec3c6fd2a..c9430b235 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -58,6 +58,7 @@
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
+ - [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
- [JustAMan](https://github.com/JustAMan)
@@ -162,6 +163,7 @@
- [vgambier](https://github.com/vgambier)
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [RealGreenDragon](https://github.com/RealGreenDragon)
+ - [ipitio](https://github.com/ipitio)
# Emby Contributors
@@ -231,3 +233,4 @@
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
+ - [JPUC1143](https://github.com/Jpuc1143/)
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 000000000..ba02b21fd
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,90 @@
+<Project>
+ <PropertyGroup>
+ <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
+ </PropertyGroup>
+
+ <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
+
+ <ItemGroup Label="Package Dependencies">
+ <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
+ <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
+ <PackageVersion Include="AutoFixture" Version="4.18.0" />
+ <PackageVersion Include="BDInfo" Version="0.7.6.2" />
+ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
+ <PackageVersion Include="BlurHashSharp" Version="1.2.0" />
+ <PackageVersion Include="CommandLineParser" Version="2.9.1" />
+ <PackageVersion Include="coverlet.collector" Version="3.2.0" />
+ <PackageVersion Include="Diacritics" Version="3.3.14" />
+ <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
+ <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" />
+ <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
+ <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
+ <PackageVersion Include="libse" Version="3.6.11" />
+ <PackageVersion Include="LrcParser" Version="2023.308.0" />
+ <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
+ <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
+ <PackageVersion Include="MimeTypes" Version="2.4.0" />
+ <PackageVersion Include="Mono.Nat" Version="3.0.4" />
+ <PackageVersion Include="Moq" Version="4.18.4" />
+ <PackageVersion Include="NEbml" Version="0.11.0" />
+ <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
+ <PackageVersion Include="PlaylistsNET" Version="1.3.1" />
+ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
+ <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
+ <PackageVersion Include="prometheus-net" Version="8.0.0" />
+ <PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
+ <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
+ <PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
+ <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
+ <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
+ <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
+ <PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" />
+ <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
+ <PackageVersion Include="SharpFuzz" Version="2.0.1" />
+ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
+ <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
+ <PackageVersion Include="SkiaSharp" Version="2.88.3" />
+ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
+ <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
+ <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
+ <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
+ <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
+ <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
+ <PackageVersion Include="System.Globalization" Version="4.3.0" />
+ <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
+ <PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
+ <PackageVersion Include="System.Text.Json" Version="7.0.2" />
+ <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
+ <PackageVersion Include="TagLibSharp" Version="2.3.0" />
+ <PackageVersion Include="TMDbLib" Version="2.0.0" />
+ <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
+ <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
+ <PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
+ <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
+ <PackageVersion Include="xunit" Version="2.4.2" />
+ </ItemGroup>
+</Project>
diff --git a/Dockerfile b/Dockerfile
index 304f79463..e51d285e1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM debian:stable-slim as app
@@ -37,7 +38,7 @@ RUN apt-get update \
&& apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers \
- jellyfin-ffmpeg \
+ jellyfin-ffmpeg5 \
openssl \
locales \
# Intel VAAPI Tone mapping dependencies:
diff --git a/Dockerfile.arm b/Dockerfile.arm
index bbb84a461..46a3e9b99 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 5572586ae..4f9d5e1fd 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs
deleted file mode 100644
index b3aad85ce..000000000
--- a/DvdLib/BigEndianBinaryReader.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Buffers.Binary;
-using System.IO;
-
-namespace DvdLib
-{
- public class BigEndianBinaryReader : BinaryReader
- {
- public BigEndianBinaryReader(Stream input)
- : base(input)
- {
- }
-
- public override ushort ReadUInt16()
- {
- return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2));
- }
-
- public override uint ReadUInt32()
- {
- return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4));
- }
- }
-}
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
deleted file mode 100644
index 1053c0089..000000000
--- a/DvdLib/DvdLib.csproj
+++ /dev/null
@@ -1,20 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
- <PropertyGroup>
- <ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid>
- </PropertyGroup>
-
- <ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
- </ItemGroup>
-
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
- <AnalysisMode>AllDisabledByDefault</AnalysisMode>
- <Nullable>disable</Nullable>
- </PropertyGroup>
-
-</Project>
diff --git a/DvdLib/Ifo/Cell.cs b/DvdLib/Ifo/Cell.cs
deleted file mode 100644
index ea0b50e43..000000000
--- a/DvdLib/Ifo/Cell.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-
-namespace DvdLib.Ifo
-{
- public class Cell
- {
- public CellPlaybackInfo PlaybackInfo { get; private set; }
-
- public CellPositionInfo PositionInfo { get; private set; }
-
- internal void ParsePlayback(BinaryReader br)
- {
- PlaybackInfo = new CellPlaybackInfo(br);
- }
-
- internal void ParsePosition(BinaryReader br)
- {
- PositionInfo = new CellPositionInfo(br);
- }
- }
-}
diff --git a/DvdLib/Ifo/CellPlaybackInfo.cs b/DvdLib/Ifo/CellPlaybackInfo.cs
deleted file mode 100644
index 6e33a0ec5..000000000
--- a/DvdLib/Ifo/CellPlaybackInfo.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-
-namespace DvdLib.Ifo
-{
- public enum BlockMode
- {
- NotInBlock = 0,
- FirstCell = 1,
- InBlock = 2,
- LastCell = 3,
- }
-
- public enum BlockType
- {
- Normal = 0,
- Angle = 1,
- }
-
- public enum PlaybackMode
- {
- Normal = 0,
- StillAfterEachVOBU = 1,
- }
-
- public class CellPlaybackInfo
- {
- public readonly BlockMode Mode;
- public readonly BlockType Type;
- public readonly bool SeamlessPlay;
- public readonly bool Interleaved;
- public readonly bool STCDiscontinuity;
- public readonly bool SeamlessAngle;
- public readonly PlaybackMode PlaybackMode;
- public readonly bool Restricted;
- public readonly byte StillTime;
- public readonly byte CommandNumber;
- public readonly DvdTime PlaybackTime;
- public readonly uint FirstSector;
- public readonly uint FirstILVUEndSector;
- public readonly uint LastVOBUStartSector;
- public readonly uint LastSector;
-
- internal CellPlaybackInfo(BinaryReader br)
- {
- br.BaseStream.Seek(0x4, SeekOrigin.Current);
- PlaybackTime = new DvdTime(br.ReadBytes(4));
- br.BaseStream.Seek(0x10, SeekOrigin.Current);
- }
- }
-}
diff --git a/DvdLib/Ifo/CellPositionInfo.cs b/DvdLib/Ifo/CellPositionInfo.cs
deleted file mode 100644
index 216aa0f77..000000000
--- a/DvdLib/Ifo/CellPositionInfo.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-
-namespace DvdLib.Ifo
-{
- public class CellPositionInfo
- {
- public readonly ushort VOBId;
- public readonly byte CellId;
-
- internal CellPositionInfo(BinaryReader br)
- {
- VOBId = br.ReadUInt16();
- br.ReadByte();
- CellId = br.ReadByte();
- }
- }
-}
diff --git a/DvdLib/Ifo/Chapter.cs b/DvdLib/Ifo/Chapter.cs
deleted file mode 100644
index e786cb553..000000000
--- a/DvdLib/Ifo/Chapter.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma warning disable CS1591
-
-namespace DvdLib.Ifo
-{
- public class Chapter
- {
- public ushort ProgramChainNumber { get; private set; }
-
- public ushort ProgramNumber { get; private set; }
-
- public uint ChapterNumber { get; private set; }
-
- public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
- {
- ProgramChainNumber = pgcNum;
- ProgramNumber = programNum;
- ChapterNumber = chapterNum;
- }
- }
-}
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
deleted file mode 100644
index 7f8ece47d..000000000
--- a/DvdLib/Ifo/Dvd.cs
+++ /dev/null
@@ -1,167 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-
-namespace DvdLib.Ifo
-{
- public class Dvd
- {
- private readonly ushort _titleSetCount;
- public readonly List<Title> Titles;
-
- private ushort _titleCount;
- public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
- public Dvd(string path)
- {
- Titles = new List<Title>();
- var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories);
-
- var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
- allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
-
- if (vmgPath == null)
- {
- foreach (var ifo in allFiles)
- {
- if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
- if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
- {
- ReadVTS(ifoNumber, ifo.FullName);
- }
- }
- }
- else
- {
- using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- using (var vmgRead = new BigEndianBinaryReader(vmgFs))
- {
- vmgFs.Seek(0x3E, SeekOrigin.Begin);
- _titleSetCount = vmgRead.ReadUInt16();
-
- // read address of TT_SRPT
- vmgFs.Seek(0xC4, SeekOrigin.Begin);
- uint ttSectorPtr = vmgRead.ReadUInt32();
- vmgFs.Seek(ttSectorPtr * 2048, SeekOrigin.Begin);
- ReadTT_SRPT(vmgRead);
- }
- }
-
- for (ushort titleSetNum = 1; titleSetNum <= _titleSetCount; titleSetNum++)
- {
- ReadVTS(titleSetNum, allFiles);
- }
- }
- }
-
- private void ReadTT_SRPT(BinaryReader read)
- {
- _titleCount = read.ReadUInt16();
- read.BaseStream.Seek(6, SeekOrigin.Current);
- for (uint titleNum = 1; titleNum <= _titleCount; titleNum++)
- {
- var t = new Title(titleNum);
- t.ParseTT_SRPT(read);
- Titles.Add(t);
- }
- }
-
- private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
- {
- var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
-
- var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
- allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
-
- if (vtsPath == null)
- {
- throw new FileNotFoundException("Unable to find VTS IFO file");
- }
-
- ReadVTS(vtsNum, vtsPath.FullName);
- }
-
- private void ReadVTS(ushort vtsNum, string vtsPath)
- {
- VTSPaths[vtsNum] = vtsPath;
-
- using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- using (var vtsRead = new BigEndianBinaryReader(vtsFs))
- {
- // Read VTS_PTT_SRPT
- vtsFs.Seek(0xC8, SeekOrigin.Begin);
- uint vtsPttSrptSecPtr = vtsRead.ReadUInt32();
- uint baseAddr = (vtsPttSrptSecPtr * 2048);
- vtsFs.Seek(baseAddr, SeekOrigin.Begin);
-
- ushort numTitles = vtsRead.ReadUInt16();
- vtsRead.ReadUInt16();
- uint endaddr = vtsRead.ReadUInt32();
- uint[] offsets = new uint[numTitles];
- for (ushort titleNum = 0; titleNum < numTitles; titleNum++)
- {
- offsets[titleNum] = vtsRead.ReadUInt32();
- }
-
- for (uint titleNum = 0; titleNum < numTitles; titleNum++)
- {
- uint chapNum = 1;
- vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
- var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
- if (t == null)
- {
- continue;
- }
-
- do
- {
- t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
- if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
- {
- break;
- }
-
- chapNum++;
- }
- while (vtsFs.Position < (baseAddr + endaddr));
- }
-
- // Read VTS_PGCI
- vtsFs.Seek(0xCC, SeekOrigin.Begin);
- uint vtsPgciSecPtr = vtsRead.ReadUInt32();
- vtsFs.Seek(vtsPgciSecPtr * 2048, SeekOrigin.Begin);
-
- long startByte = vtsFs.Position;
-
- ushort numPgcs = vtsRead.ReadUInt16();
- vtsFs.Seek(6, SeekOrigin.Current);
- for (ushort pgcNum = 1; pgcNum <= numPgcs; pgcNum++)
- {
- byte pgcCat = vtsRead.ReadByte();
- bool entryPgc = (pgcCat & 0x80) != 0;
- uint titleNum = (uint)(pgcCat & 0x7F);
-
- vtsFs.Seek(3, SeekOrigin.Current);
- uint vtsPgcOffset = vtsRead.ReadUInt32();
-
- var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
- if (t != null)
- {
- t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
- }
- }
- }
- }
- }
- }
-}
diff --git a/DvdLib/Ifo/DvdTime.cs b/DvdLib/Ifo/DvdTime.cs
deleted file mode 100644
index d23140610..000000000
--- a/DvdLib/Ifo/DvdTime.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace DvdLib.Ifo
-{
- public class DvdTime
- {
- public readonly byte Hour, Minute, Second, Frames, FrameRate;
-
- public DvdTime(byte[] data)
- {
- Hour = GetBCDValue(data[0]);
- Minute = GetBCDValue(data[1]);
- Second = GetBCDValue(data[2]);
- Frames = GetBCDValue((byte)(data[3] & 0x3F));
-
- if ((data[3] & 0x80) != 0)
- {
- FrameRate = 30;
- }
- else if ((data[3] & 0x40) != 0)
- {
- FrameRate = 25;
- }
- }
-
- private static byte GetBCDValue(byte data)
- {
- return (byte)((((data & 0xF0) >> 4) * 10) + (data & 0x0F));
- }
-
- public static explicit operator TimeSpan(DvdTime time)
- {
- int ms = (int)(((1.0 / (double)time.FrameRate) * time.Frames) * 1000.0);
- return new TimeSpan(0, time.Hour, time.Minute, time.Second, ms);
- }
- }
-}
diff --git a/DvdLib/Ifo/Program.cs b/DvdLib/Ifo/Program.cs
deleted file mode 100644
index 3d94fa7dc..000000000
--- a/DvdLib/Ifo/Program.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace DvdLib.Ifo
-{
- public class Program
- {
- public IReadOnlyList<Cell> Cells { get; }
-
- public Program(List<Cell> cells)
- {
- Cells = cells;
- }
- }
-}
diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs
deleted file mode 100644
index 83c0051b9..000000000
--- a/DvdLib/Ifo/ProgramChain.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace DvdLib.Ifo
-{
- public enum ProgramPlaybackMode
- {
- Sequential,
- Random,
- Shuffle
- }
-
- public class ProgramChain
- {
- private byte _programCount;
- public readonly List<Program> Programs;
-
- private byte _cellCount;
- public readonly List<Cell> Cells;
-
- public DvdTime PlaybackTime { get; private set; }
-
- public UserOperation ProhibitedUserOperations { get; private set; }
-
- public byte[] AudioStreamControl { get; private set; } // 8*2 entries
- public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
-
- private ushort _nextProgramNumber;
-
- private ushort _prevProgramNumber;
-
- private ushort _goupProgramNumber;
-
- public ProgramPlaybackMode PlaybackMode { get; private set; }
-
- public uint ProgramCount { get; private set; }
-
- public byte StillTime { get; private set; }
-
- public byte[] Palette { get; private set; } // 16*4 entries
-
- private ushort _commandTableOffset;
-
- private ushort _programMapOffset;
- private ushort _cellPlaybackOffset;
- private ushort _cellPositionOffset;
-
- public readonly uint VideoTitleSetIndex;
-
- internal ProgramChain(uint vtsPgcNum)
- {
- VideoTitleSetIndex = vtsPgcNum;
- Cells = new List<Cell>();
- Programs = new List<Program>();
- }
-
- internal void ParseHeader(BinaryReader br)
- {
- long startPos = br.BaseStream.Position;
-
- br.ReadUInt16();
- _programCount = br.ReadByte();
- _cellCount = br.ReadByte();
- PlaybackTime = new DvdTime(br.ReadBytes(4));
- ProhibitedUserOperations = (UserOperation)br.ReadUInt32();
- AudioStreamControl = br.ReadBytes(16);
- SubpictureStreamControl = br.ReadBytes(128);
-
- _nextProgramNumber = br.ReadUInt16();
- _prevProgramNumber = br.ReadUInt16();
- _goupProgramNumber = br.ReadUInt16();
-
- StillTime = br.ReadByte();
- byte pbMode = br.ReadByte();
- if (pbMode == 0)
- {
- PlaybackMode = ProgramPlaybackMode.Sequential;
- }
- else
- {
- PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
- }
-
- ProgramCount = (uint)(pbMode & 0x7F);
-
- Palette = br.ReadBytes(64);
- _commandTableOffset = br.ReadUInt16();
- _programMapOffset = br.ReadUInt16();
- _cellPlaybackOffset = br.ReadUInt16();
- _cellPositionOffset = br.ReadUInt16();
-
- // read position info
- br.BaseStream.Seek(startPos + _cellPositionOffset, SeekOrigin.Begin);
- for (int cellNum = 0; cellNum < _cellCount; cellNum++)
- {
- var c = new Cell();
- c.ParsePosition(br);
- Cells.Add(c);
- }
-
- br.BaseStream.Seek(startPos + _cellPlaybackOffset, SeekOrigin.Begin);
- for (int cellNum = 0; cellNum < _cellCount; cellNum++)
- {
- Cells[cellNum].ParsePlayback(br);
- }
-
- br.BaseStream.Seek(startPos + _programMapOffset, SeekOrigin.Begin);
- var cellNumbers = new List<int>();
- for (int progNum = 0; progNum < _programCount; progNum++) cellNumbers.Add(br.ReadByte() - 1);
-
- for (int i = 0; i < cellNumbers.Count; i++)
- {
- int max = (i + 1 == cellNumbers.Count) ? _cellCount : cellNumbers[i + 1];
- Programs.Add(new Program(Cells.Where((c, idx) => idx >= cellNumbers[i] && idx < max).ToList()));
- }
- }
- }
-}
diff --git a/DvdLib/Ifo/Title.cs b/DvdLib/Ifo/Title.cs
deleted file mode 100644
index 29a0b95c7..000000000
--- a/DvdLib/Ifo/Title.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.IO;
-
-namespace DvdLib.Ifo
-{
- public class Title
- {
- public uint TitleNumber { get; private set; }
-
- public uint AngleCount { get; private set; }
-
- public ushort ChapterCount { get; private set; }
-
- public byte VideoTitleSetNumber { get; private set; }
-
- private ushort _parentalManagementMask;
- private byte _titleNumberInVTS;
- private uint _vtsStartSector; // relative to start of entire disk
-
- public ProgramChain EntryProgramChain { get; private set; }
-
- public readonly List<ProgramChain> ProgramChains;
-
- public readonly List<Chapter> Chapters;
-
- public Title(uint titleNum)
- {
- ProgramChains = new List<ProgramChain>();
- Chapters = new List<Chapter>();
- Chapters = new List<Chapter>();
- TitleNumber = titleNum;
- }
-
- public bool IsVTSTitle(uint vtsNum, uint vtsTitleNum)
- {
- return (vtsNum == VideoTitleSetNumber && vtsTitleNum == _titleNumberInVTS);
- }
-
- internal void ParseTT_SRPT(BinaryReader br)
- {
- byte titleType = br.ReadByte();
- // TODO parse Title Type
-
- AngleCount = br.ReadByte();
- ChapterCount = br.ReadUInt16();
- _parentalManagementMask = br.ReadUInt16();
- VideoTitleSetNumber = br.ReadByte();
- _titleNumberInVTS = br.ReadByte();
- _vtsStartSector = br.ReadUInt32();
- }
-
- internal void AddPgc(BinaryReader br, long startByte, bool entryPgc, uint pgcNum)
- {
- long curPos = br.BaseStream.Position;
- br.BaseStream.Seek(startByte, SeekOrigin.Begin);
-
- var pgc = new ProgramChain(pgcNum);
- pgc.ParseHeader(br);
- ProgramChains.Add(pgc);
- if (entryPgc)
- {
- EntryProgramChain = pgc;
- }
-
- br.BaseStream.Seek(curPos, SeekOrigin.Begin);
- }
- }
-}
diff --git a/DvdLib/Ifo/UserOperation.cs b/DvdLib/Ifo/UserOperation.cs
deleted file mode 100644
index 5d111ebc0..000000000
--- a/DvdLib/Ifo/UserOperation.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace DvdLib.Ifo
-{
- [Flags]
- public enum UserOperation
- {
- None = 0,
- TitleOrTimePlay = 1,
- ChapterSearchOrPlay = 2,
- TitlePlay = 4,
- Stop = 8,
- GoUp = 16,
- TimeOrChapterSearch = 32,
- PrevOrTopProgramSearch = 64,
- NextProgramSearch = 128,
- ForwardScan = 256,
- BackwardScan = 512,
- TitleMenuCall = 1024,
- RootMenuCall = 2048,
- SubpictureMenuCall = 4096,
- AudioMenuCall = 8192,
- AngleMenuCall = 16384,
- ChapterMenuCall = 32768,
- Resume = 65536,
- ButtonSelectOrActive = 131072,
- StillOff = 262144,
- PauseOn = 524288,
- AudioStreamChange = 1048576,
- SubpictureStreamChange = 2097152,
- AngleChange = 4194304,
- KaraokeAudioPresentationModeChange = 8388608,
- VideoPresentationModeChange = 16777216,
- }
-}
diff --git a/DvdLib/Properties/AssemblyInfo.cs b/DvdLib/Properties/AssemblyInfo.cs
deleted file mode 100644
index 6acd571d6..000000000
--- a/DvdLib/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("DvdLib")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin Server")]
-[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
index c484dac54..db1190ae7 100644
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
+++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
@@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
- var list = new List<StateVariable>
+ return new StateVariable[]
{
new StateVariable
{
@@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
SendsEvents = false
}
};
-
- return list;
}
}
}
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
index 3edaabb70..9af28aa7c 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
@@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
- var list = new List<StateVariable>
+ return new StateVariable[]
{
new StateVariable
{
@@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
SendsEvents = false
}
};
-
- return list;
}
}
}
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 7ffb7118a..aca239964 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -28,13 +28,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@@ -80,7 +80,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 2dc079254..aab475153 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -7,6 +7,7 @@ using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
+using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
@@ -262,7 +263,7 @@ namespace Emby.Dlna.Main
{
_publisher = new SsdpDevicePublisher(
_communicationsServer,
- MediaBrowser.Common.System.OperatingSystem.Name,
+ Environment.OSVersion.Platform.ToString(),
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 7b1f942c5..86db36337 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
- IMediaEncoder mediaEncoder)
+ IMediaEncoder mediaEncoder,
+ Device device)
{
_session = session;
_sessionManager = sessionManager;
@@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
_localization = localization;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
- }
-
- public bool IsSessionActive => !_disposed && _device is not null;
- public bool SupportsMediaControl => IsSessionActive;
-
- public void Init(Device device)
- {
_device = device;
_device.OnDeviceUnavailable = OnDeviceUnavailable;
_device.PlaybackStart += OnDevicePlaybackStart;
@@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
+ public bool IsSessionActive => !_disposed;
+
+ public bool SupportsMediaControl => IsSessionActive;
+
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
@@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo
}
}
- private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
+ private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
{
var info = e.Argument;
if (!_disposed
- && info.Headers.TryGetValue("USN", out string usn)
+ && info.Headers.TryGetValue("USN", out string? usn)
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
- || (info.Headers.TryGetValue("NT", out string nt)
+ || (info.Headers.TryGetValue("NT", out string? nt)
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
{
OnDeviceUnavailable();
}
}
- private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
+ private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
{
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
@@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
+ private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
{
if (_disposed)
{
@@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
+ private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
{
if (_disposed)
{
@@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (_disposed)
{
@@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem CreatePlaylistItem(
BaseItem item,
- User user,
+ User? user,
long startPostionTicks,
- string mediaSourceId,
+ string? mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex)
{
@@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
return playlistItem;
}
- private string GetDlnaHeaders(PlaylistItem item)
+ private string? GetDlnaHeaders(PlaylistItem item)
{
var profile = item.Profile;
var streamInfo = item.StreamInfo;
@@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+ private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
@@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null;
- _device = null;
_disposed = true;
}
@@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
- if (command.Arguments.TryGetValue("Index", out string index))
+ if (command.Arguments.TryGetValue("Index", out string? index))
{
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
@@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
- if (command.Arguments.TryGetValue("Volume", out string vol))
+ if (command.Arguments.TryGetValue("Volume", out string? vol))
{
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
@@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
throw new ObjectDisposedException(GetType().Name);
}
- if (_device is null)
- {
- return Task.CompletedTask;
- }
-
- if (name == SessionMessageType.Play)
- {
- return SendPlayCommand(data as PlayRequest, cancellationToken);
- }
-
- if (name == SessionMessageType.Playstate)
+ return name switch
{
- return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
- }
-
- if (name == SessionMessageType.GeneralCommand)
- {
- return SendGeneralCommand(data as GeneralCommand, cancellationToken);
- }
-
- // Not supported or needed right now
- return Task.CompletedTask;
+ SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
+ SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
+ SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
+ _ => Task.CompletedTask // Not supported or needed right now
+ };
}
private class StreamParams
{
- private MediaSourceInfo _mediaSource;
- private IMediaSourceManager _mediaSourceManager;
+ private MediaSourceInfo? _mediaSource;
+ private IMediaSourceManager? _mediaSourceManager;
public Guid ItemId { get; set; }
@@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
public int? SubtitleStreamIndex { get; set; }
- public string DeviceProfileId { get; set; }
+ public string? DeviceProfileId { get; set; }
- public string DeviceId { get; set; }
+ public string? DeviceId { get; set; }
- public string MediaSourceId { get; set; }
+ public string? MediaSourceId { get; set; }
- public string LiveStreamId { get; set; }
+ public string? LiveStreamId { get; set; }
- public BaseItem Item { get; set; }
+ public BaseItem? Item { get; set; }
- public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
+ public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
{
if (_mediaSource is not null)
{
@@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
{
var part = parts[i];
- if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{
if (Guid.TryParse(parts[i + 1], out var result))
{
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index f4a9a90af..b469c9cb0 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
_userDataManager,
_localization,
_mediaSourceManager,
- _mediaEncoder);
+ _mediaEncoder,
+ device);
sessionInfo.AddController(controller);
- controller.Init(device);
-
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile();
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index d00df781d..69ef6f645 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -147,11 +147,16 @@ namespace Emby.Dlna.Server
}
}
- private string GetFriendlyName()
+ internal string GetFriendlyName()
{
if (string.IsNullOrEmpty(_profile.FriendlyName))
{
- return "Jellyfin - " + _serverName;
+ return _serverName;
+ }
+
+ if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
+ {
+ return _profile.FriendlyName;
}
var characterList = new List<char>();
@@ -164,13 +169,18 @@ namespace Emby.Dlna.Server
}
}
- var characters = characterList.ToArray();
-
- var serverName = new string(characters);
-
- var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
+ var serverName = string.Create(
+ characterList.Count,
+ characterList,
+ (dest, source) =>
+ {
+ for (int i = 0; i < dest.Length; i++)
+ {
+ dest[i] = source[i];
+ }
+ });
- return name ?? string.Empty;
+ return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
}
private void AppendIconList(StringBuilder builder)
diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs
index bbfdccc90..86a564153 100644
--- a/Emby.Naming/Audio/AlbumParser.cs
+++ b/Emby.Naming/Audio/AlbumParser.cs
@@ -3,6 +3,7 @@ using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{
@@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
var tmp = trimmedFilename.Slice(prefix.Length).Trim();
- int index = tmp.IndexOf(' ');
- if (index != -1)
- {
- tmp = tmp.Slice(0, index);
- }
-
- if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
+ if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
{
return true;
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 7b4429ab1..75fdedfea 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions)
{
- var match = new Regex(expression, RegexOptions.IgnoreCase).Match(fileName);
+ var match = Regex.Match(fileName, expression, RegexOptions.IgnoreCase);
if (match.Success)
{
if (!result.ChapterNumber.HasValue)
@@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["chapter"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.ChapterNumber = intValue;
}
@@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["part"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.PartNumber = intValue;
}
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index bdae20b6b..ca304102f 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook
{
if (group.Count() > 1 || haveChaptersOrPages)
{
- var ex = new List<AudioBookFileInfo>();
- var alt = new List<AudioBookFileInfo>();
+ List<AudioBookFileInfo>? ex = null;
+ List<AudioBookFileInfo>? alt = null;
foreach (var audioFile in group)
{
- var name = Path.GetFileNameWithoutExtension(audioFile.Path);
- if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
- name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
- name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
+ var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan());
+ if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase)
+ || name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase)
+ || name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{
- alt.Add(audioFile);
+ (alt ??= new()).Add(audioFile);
}
else
{
- ex.Add(audioFile);
+ (ex ??= new()).Add(audioFile);
}
}
- if (ex.Count > 0)
+ if (ex is not null)
{
var extra = ex
.OrderBy(x => x.Container)
@@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
extras.AddRange(extra);
}
- if (alt.Count > 0)
+ if (alt is not null)
{
var alternatives = alt
.OrderBy(x => x.Container)
diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs
index 97b34199e..5ea649dbf 100644
--- a/Emby.Naming/AudioBook/AudioBookNameParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs
@@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook
AudioBookNameParserResult result = default;
foreach (var expression in _options.AudioBookNamesExpressions)
{
- var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
+ var match = Regex.Match(name, expression, RegexOptions.IgnoreCase);
if (match.Success)
{
if (result.Name is null)
@@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["year"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.Year = intValue;
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 54f62a157..e9161a6b7 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -338,7 +338,15 @@ namespace Emby.Naming.Common
}
},
- // This isn't a Kodi naming rule, but the expression below causes false positives,
+ // This isn't a Kodi naming rule, but the expression below causes false episode numbers for
+ // Title Season X Episode X naming schemes.
+ // "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi"
+ new EpisodeExpression(@".*[\\\/]((?<seriesname>[^\\/]+?)\s)?[Ss](?:eason)?\s*(?<seasonnumber>[0-9]+)\s+[Ee](?:pisode)?\s*(?<epnumber>[0-9]+).*$")
+ {
+ IsNamed = true
+ },
+
+ // Not a Kodi rule as well, but the expression below also causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
@@ -453,16 +461,6 @@ namespace Emby.Naming.Common
},
};
- EpisodeWithoutSeasonExpressions = new[]
- {
- @"[/\._ \-]()([0-9]+)(-[0-9]+)?"
- };
-
- EpisodeMultiPartExpressions = new[]
- {
- @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)"
- };
-
VideoExtraRules = new[]
{
new ExtraRule(
@@ -798,16 +796,6 @@ namespace Emby.Naming.Common
public EpisodeExpression[] EpisodeExpressions { get; set; }
/// <summary>
- /// Gets or sets list of raw episode without season regular expressions strings.
- /// </summary>
- public string[] EpisodeWithoutSeasonExpressions { get; set; }
-
- /// <summary>
- /// Gets or sets list of raw multi-part episodes regular expressions strings.
- /// </summary>
- public string[] EpisodeMultiPartExpressions { get; set; }
-
- /// <summary>
/// Gets or sets list of video file extensions.
/// </summary>
public string[] VideoFileExtensions { get; set; }
@@ -878,24 +866,12 @@ namespace Emby.Naming.Common
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
- /// Gets list of episode without season regular expressions.
- /// </summary>
- public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
-
- /// <summary>
- /// Gets list of multi-part episode regular expressions.
- /// </summary>
- public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
-
- /// <summary>
/// Compiles raw regex strings into regexes.
/// </summary>
public void Compile()
{
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
- EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
- EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
}
private Regex Compile(string exp)
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 80bc57a5d..f3973dad9 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -42,18 +42,18 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs
index d706be280..8cd5a126e 100644
--- a/Emby.Naming/TV/EpisodePathParser.cs
+++ b/Emby.Naming/TV/EpisodePathParser.cs
@@ -113,7 +113,7 @@ namespace Emby.Naming.TV
if (expression.DateTimeFormats.Length > 0)
{
if (DateTime.TryParseExact(
- match.Groups[0].Value,
+ match.Groups[0].ValueSpan,
expression.DateTimeFormats,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
@@ -125,7 +125,7 @@ namespace Emby.Naming.TV
result.Success = true;
}
}
- else if (DateTime.TryParse(match.Groups[0].Value, out date))
+ else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
{
result.Year = date.Year;
result.Month = date.Month;
@@ -138,12 +138,12 @@ namespace Emby.Naming.TV
}
else if (expression.IsNamed)
{
- if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
- if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}
@@ -158,7 +158,7 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
{
- if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EndingEpisodeNumber = num;
}
@@ -170,12 +170,12 @@ namespace Emby.Naming.TV
}
else
{
- if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
- if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
index 156a03c9e..307a84096 100644
--- a/Emby.Naming/TV/SeriesResolver.cs
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -14,7 +14,7 @@ namespace Emby.Naming.TV
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving namings like "S.H.O.W".
/// </summary>
- private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
+ private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
/// <summary>
/// Resolve information about series from path.
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs
index 0ee633dcc..9a6c6e978 100644
--- a/Emby.Naming/Video/CleanDateTimeParser.cs
+++ b/Emby.Naming/Video/CleanDateTimeParser.cs
@@ -43,7 +43,7 @@ namespace Emby.Naming.Video
&& match.Groups.Count == 5
&& match.Groups[1].Success
&& match.Groups[2].Success
- && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
+ && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
return true;
diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index 21d0da364..3219472ef 100644
--- a/Emby.Naming/Video/ExtraRuleResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -56,7 +56,7 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
- var filename = Path.GetFileName(path);
+ var filename = Path.GetFileName(path.AsSpan());
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs
index 76b487f42..be0f79d33 100644
--- a/Emby.Naming/Video/FileStackRule.cs
+++ b/Emby.Naming/Video/FileStackRule.cs
@@ -17,7 +17,7 @@ public class FileStackRule
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
public FileStackRule(string token, bool isNumerical)
{
- _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+ _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
IsNumerical = isNumerical;
}
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 804832040..6209cd46f 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -13,6 +14,8 @@ namespace Emby.Naming.Video
/// </summary>
public static class VideoListResolver
{
+ private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
@@ -106,6 +109,7 @@ namespace Emby.Naming.Video
}
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+ VideoInfo? primary = null;
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
@@ -114,29 +118,43 @@ namespace Emby.Naming.Video
continue;
}
- if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
{
return videos;
}
+
+ if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
+ {
+ primary = video;
+ }
+ }
+
+ if (videos.Count > 1)
+ {
+ var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
+ videos.Clear();
+ foreach (var group in groups)
+ {
+ if (group.Key)
+ {
+ videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
+ }
+ else
+ {
+ videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
+ }
+ }
}
- // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
- videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
+ primary ??= videos[0];
+ videos.Remove(primary);
var list = new List<VideoInfo>
{
- videos[0]
+ primary
};
- var alternateVersionsLen = videos.Count - 1;
- var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- for (int i = 0; i < alternateVersionsLen; i++)
- {
- var video = videos[i + 1];
- alternateVersions[i] = video.Files[0];
- }
-
- list[0].AlternateVersions = alternateVersions;
+ list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
list[0].Name = folderName.ToString();
return list;
@@ -161,9 +179,8 @@ namespace Emby.Naming.Video
return true;
}
- private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
+ private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
{
- var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
return false;
@@ -176,16 +193,15 @@ namespace Emby.Naming.Video
}
// There are no span overloads for regex unfortunately
- var tmpTestFilename = testFilename.ToString();
- if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+ if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
{
- tmpTestFilename = cleanName.Trim();
+ testFilename = cleanName.AsSpan().Trim();
}
// The CleanStringParser should have removed common keywords etc.
- return string.IsNullOrEmpty(tmpTestFilename)
+ return testFilename.IsEmpty
|| testFilename[0] == '-'
- || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+ || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
}
}
diff --git a/Emby.Notifications/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs
deleted file mode 100644
index 35aac3a11..000000000
--- a/Emby.Notifications/CoreNotificationTypes.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Notifications;
-
-namespace Emby.Notifications
-{
- public class CoreNotificationTypes : INotificationTypeFactory
- {
- private readonly ILocalizationManager _localization;
-
- public CoreNotificationTypes(ILocalizationManager localization)
- {
- _localization = localization;
- }
-
- public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
- {
- var knownTypes = new NotificationTypeInfo[]
- {
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.ApplicationUpdateInstalled)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.InstallationFailed)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.PluginInstalled)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.PluginError)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.PluginUninstalled)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.PluginUpdateInstalled)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.ServerRestartRequired)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.TaskFailed)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.NewLibraryContent)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.AudioPlayback)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.VideoPlayback)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.AudioPlaybackStopped)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.VideoPlaybackStopped)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.UserLockedOut)
- },
- new NotificationTypeInfo
- {
- Type = nameof(NotificationType.ApplicationUpdateAvailable)
- }
- };
-
- foreach (var type in knownTypes)
- {
- Update(type);
- }
-
- var systemName = _localization.GetLocalizedString("System");
-
- return knownTypes.OrderByDescending(i => string.Equals(i.Category, systemName, StringComparison.OrdinalIgnoreCase))
- .ThenBy(i => i.Category)
- .ThenBy(i => i.Name);
- }
-
- private void Update(NotificationTypeInfo note)
- {
- note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type);
-
- note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
-
- if (note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1)
- {
- note.Category = _localization.GetLocalizedString("User");
- }
- else if (note.Type.IndexOf("Plugin", StringComparison.OrdinalIgnoreCase) != -1)
- {
- note.Category = _localization.GetLocalizedString("Plugin");
- }
- else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
- {
- note.Category = _localization.GetLocalizedString("User");
- }
- else
- {
- note.Category = _localization.GetLocalizedString("System");
- }
- }
- }
-}
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
deleted file mode 100644
index 138965c89..000000000
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ /dev/null
@@ -1,35 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
- <PropertyGroup>
- <ProjectGuid>{2E030C33-6923-4530-9E54-FA29FA6AD1A9}</ProjectGuid>
- </PropertyGroup>
-
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
- </PropertyGroup>
-
- <ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
- </ItemGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
- </ItemGroup>
-
- <!-- Code analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
-</Project>
diff --git a/Emby.Notifications/NotificationConfigurationFactory.cs b/Emby.Notifications/NotificationConfigurationFactory.cs
deleted file mode 100644
index 3fb3553d0..000000000
--- a/Emby.Notifications/NotificationConfigurationFactory.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Notifications;
-
-namespace Emby.Notifications
-{
- public class NotificationConfigurationFactory : IConfigurationFactory
- {
- public IEnumerable<ConfigurationStore> GetConfigurations()
- {
- return new ConfigurationStore[]
- {
- new ConfigurationStore
- {
- Key = "notifications",
- ConfigurationType = typeof(NotificationOptions)
- }
- };
- }
- }
-}
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
deleted file mode 100644
index 3763b1e92..000000000
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ /dev/null
@@ -1,314 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using Jellyfin.Extensions;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Notifications;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Notifications
-{
- /// <summary>
- /// Creates notifications for various system events.
- /// </summary>
- public class NotificationEntryPoint : IServerEntryPoint
- {
- private readonly ILogger<NotificationEntryPoint> _logger;
- private readonly IActivityManager _activityManager;
- private readonly ILocalizationManager _localization;
- private readonly INotificationManager _notificationManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IServerApplicationHost _appHost;
- private readonly IConfigurationManager _config;
-
- private readonly object _libraryChangedSyncLock = new object();
- private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
-
- private Timer? _libraryUpdateTimer;
-
- private string[] _coreNotificationTypes;
-
- private bool _disposed = false;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="NotificationEntryPoint" /> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="activityManager">The activity manager.</param>
- /// <param name="localization">The localization manager.</param>
- /// <param name="notificationManager">The notification manager.</param>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="config">The configuration manager.</param>
- public NotificationEntryPoint(
- ILogger<NotificationEntryPoint> logger,
- IActivityManager activityManager,
- ILocalizationManager localization,
- INotificationManager notificationManager,
- ILibraryManager libraryManager,
- IServerApplicationHost appHost,
- IConfigurationManager config)
- {
- _logger = logger;
- _activityManager = activityManager;
- _localization = localization;
- _notificationManager = notificationManager;
- _libraryManager = libraryManager;
- _appHost = appHost;
- _config = config;
-
- _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
- _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
- _activityManager.EntryCreated += OnActivityManagerEntryCreated;
-
- return Task.CompletedTask;
- }
-
- private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
- {
- var type = NotificationType.ServerRestartRequired.ToString();
-
- var notification = new NotificationRequest
- {
- NotificationType = type,
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("ServerNameNeedsToBeRestarted"),
- _appHost.Name)
- };
-
- await SendNotification(notification, null).ConfigureAwait(false);
- }
-
- private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
- {
- var entry = e.Argument;
-
- var type = entry.Type;
-
- if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- var userId = e.Argument.UserId;
-
- if (!userId.Equals(default) && !GetOptions().IsEnabledToMonitorUser(type, userId))
- {
- return;
- }
-
- var notification = new NotificationRequest
- {
- NotificationType = type,
- Name = entry.Name,
- Description = entry.Overview
- };
-
- await SendNotification(notification, null).ConfigureAwait(false);
- }
-
- private NotificationOptions GetOptions()
- {
- return _config.GetConfiguration<NotificationOptions>("notifications");
- }
-
- private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
- {
- if (!FilterItem(e.Item))
- {
- return;
- }
-
- lock (_libraryChangedSyncLock)
- {
- if (_libraryUpdateTimer is null)
- {
- _libraryUpdateTimer = new Timer(
- LibraryUpdateTimerCallback,
- null,
- 5000,
- Timeout.Infinite);
- }
- else
- {
- _libraryUpdateTimer.Change(5000, Timeout.Infinite);
- }
-
- _itemsAdded.Add(e.Item);
- }
- }
-
- private bool FilterItem(BaseItem item)
- {
- if (item.IsFolder)
- {
- return false;
- }
-
- if (!item.HasPathProtocol)
- {
- return false;
- }
-
- if (item is IItemByName)
- {
- return false;
- }
-
- return item.SourceType == SourceType.Library;
- }
-
- private async void LibraryUpdateTimerCallback(object? state)
- {
- List<BaseItem> items;
-
- lock (_libraryChangedSyncLock)
- {
- items = _itemsAdded.ToList();
- _itemsAdded.Clear();
- _libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
- _libraryUpdateTimer = null;
- }
-
- if (items.Count > 10)
- {
- items = items.GetRange(0, 10);
- }
-
- foreach (var item in items)
- {
- var notification = new NotificationRequest
- {
- NotificationType = NotificationType.NewLibraryContent.ToString(),
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("ValueHasBeenAddedToLibrary"),
- GetItemName(item)),
- Description = item.Overview
- };
-
- await SendNotification(notification, item).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Creates a human readable name for the item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>A human readable name for the item.</returns>
- public static string GetItemName(BaseItem item)
- {
- var name = item.Name;
- if (item is Episode episode)
- {
- if (episode.IndexNumber.HasValue)
- {
- name = string.Format(
- CultureInfo.InvariantCulture,
- "Ep{0} - {1}",
- episode.IndexNumber.Value,
- name);
- }
-
- if (episode.ParentIndexNumber.HasValue)
- {
- name = string.Format(
- CultureInfo.InvariantCulture,
- "S{0}, {1}",
- episode.ParentIndexNumber.Value,
- name);
- }
- }
-
- if (item is IHasSeries hasSeries)
- {
- name = hasSeries.SeriesName + " - " + name;
- }
-
- if (item is IHasAlbumArtist hasAlbumArtist)
- {
- var artists = hasAlbumArtist.AlbumArtists;
-
- if (artists.Count > 0)
- {
- name = artists[0] + " - " + name;
- }
- }
- else if (item is IHasArtist hasArtist)
- {
- var artists = hasArtist.Artists;
-
- if (artists.Count > 0)
- {
- name = artists[0] + " - " + name;
- }
- }
-
- return name;
- }
-
- private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
- {
- try
- {
- await _notificationManager.SendNotification(notification, relatedItem, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error sending notification");
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and optionally managed resources.
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _libraryUpdateTimer?.Dispose();
- }
-
- _libraryUpdateTimer = null;
-
- _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
- _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
- _activityManager.EntryCreated -= OnActivityManagerEntryCreated;
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs
deleted file mode 100644
index a5a76b2af..000000000
--- a/Emby.Notifications/NotificationManager.cs
+++ /dev/null
@@ -1,224 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Notifications
-{
- /// <summary>
- /// NotificationManager class.
- /// </summary>
- public class NotificationManager : INotificationManager
- {
- private readonly ILogger<NotificationManager> _logger;
- private readonly IUserManager _userManager;
- private readonly IServerConfigurationManager _config;
-
- private INotificationService[] _services = Array.Empty<INotificationService>();
- private INotificationTypeFactory[] _typeFactories = Array.Empty<INotificationTypeFactory>();
-
- /// <summary>
- /// Initializes a new instance of the <see cref="NotificationManager" /> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="userManager">The user manager.</param>
- /// <param name="config">The server configuration manager.</param>
- public NotificationManager(
- ILogger<NotificationManager> logger,
- IUserManager userManager,
- IServerConfigurationManager config)
- {
- _logger = logger;
- _userManager = userManager;
- _config = config;
- }
-
- private NotificationOptions GetConfiguration()
- {
- return _config.GetConfiguration<NotificationOptions>("notifications");
- }
-
- /// <inheritdoc />
- public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
- {
- return SendNotification(request, null, cancellationToken);
- }
-
- /// <inheritdoc />
- public Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken)
- {
- var notificationType = request.NotificationType;
-
- var options = string.IsNullOrEmpty(notificationType) ?
- null :
- GetConfiguration().GetOptions(notificationType);
-
- var users = GetUserIds(request, options)
- .Select(i => _userManager.GetUserById(i))
- .Where(i => relatedItem is null || relatedItem.IsVisibleStandalone(i))
- .ToArray();
-
- var title = request.Name;
- var description = request.Description;
-
- var tasks = _services.Where(i => IsEnabled(i, notificationType))
- .Select(i => SendNotification(request, i, users, title, description, cancellationToken));
-
- return Task.WhenAll(tasks);
- }
-
- private Task SendNotification(
- NotificationRequest request,
- INotificationService service,
- IEnumerable<User> users,
- string title,
- string description,
- CancellationToken cancellationToken)
- {
- users = users.Where(i => IsEnabledForUser(service, i));
-
- var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken));
-
- return Task.WhenAll(tasks);
- }
-
- private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options)
- {
- if (request.SendToUserMode.HasValue)
- {
- switch (request.SendToUserMode.Value)
- {
- case SendToUserType.Admins:
- return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
- .Select(i => i.Id);
- case SendToUserType.All:
- return _userManager.UsersIds;
- case SendToUserType.Custom:
- return request.UserIds;
- default:
- throw new ArgumentException("Unrecognized SendToUserMode: " + request.SendToUserMode.Value);
- }
- }
-
- if (options is not null && !string.IsNullOrEmpty(request.NotificationType))
- {
- var config = GetConfiguration();
-
- return _userManager.Users
- .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i))
- .Select(i => i.Id);
- }
-
- return request.UserIds;
- }
-
- private async Task SendNotification(
- NotificationRequest request,
- INotificationService service,
- string title,
- string description,
- User user,
- CancellationToken cancellationToken)
- {
- var notification = new UserNotification
- {
- Date = request.Date,
- Description = description,
- Level = request.Level,
- Name = title,
- Url = request.Url,
- User = user
- };
-
- _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username);
-
- try
- {
- await service.SendNotification(notification, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error sending notification to {0}", service.Name);
- }
- }
-
- private bool IsEnabledForUser(INotificationService service, User user)
- {
- try
- {
- return service.IsEnabledForUser(user);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in IsEnabledForUser");
- return false;
- }
- }
-
- private bool IsEnabled(INotificationService service, string notificationType)
- {
- if (string.IsNullOrEmpty(notificationType))
- {
- return true;
- }
-
- return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
- }
-
- /// <inheritdoc />
- public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
- {
- _services = services.ToArray();
- _typeFactories = notificationTypeFactories.ToArray();
- }
-
- /// <inheritdoc />
- public List<NotificationTypeInfo> GetNotificationTypes()
- {
- var list = _typeFactories.Select(i =>
- {
- try
- {
- return i.GetNotificationTypes().ToList();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in GetNotificationTypes");
- return new List<NotificationTypeInfo>();
- }
- }).SelectMany(i => i).ToList();
-
- var config = GetConfiguration();
-
- foreach (var i in list)
- {
- i.Enabled = config.IsEnabled(i.Type);
- }
-
- return list;
- }
-
- /// <inheritdoc />
- public IEnumerable<NameIdPair> GetNotificationServices()
- {
- return _services.Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
- }).OrderBy(i => i.Name);
- }
- }
-}
diff --git a/Emby.Notifications/Properties/AssemblyInfo.cs b/Emby.Notifications/Properties/AssemblyInfo.cs
deleted file mode 100644
index 5c82c90c4..000000000
--- a/Emby.Notifications/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Emby.Notifications")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin Server")]
-[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 34bc8f32f..0f97a0686 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -15,7 +15,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="TagLibSharp" Version="2.3.0" />
+ <PackageReference Include="TagLibSharp" />
</ItemGroup>
<PropertyGroup>
@@ -26,13 +26,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 985a127d5..a4deeddb7 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -34,14 +32,9 @@ namespace Emby.Server.Implementations.AppBase
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
/// <summary>
- /// The _configuration loaded.
- /// </summary>
- private bool _configurationLoaded;
-
- /// <summary>
/// The _configuration.
/// </summary>
- private BaseApplicationConfiguration _configuration;
+ private BaseApplicationConfiguration? _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
/// <summary>
/// Occurs when [configuration updated].
/// </summary>
- public event EventHandler<EventArgs> ConfigurationUpdated;
+ public event EventHandler<EventArgs>? ConfigurationUpdated;
/// <summary>
/// Occurs when [configuration updating].
/// </summary>
- public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+ public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
/// <summary>
/// Occurs when [named configuration updated].
/// </summary>
- public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+ public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
/// <summary>
/// Gets the type of the configuration.
@@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
{
get
{
- if (_configurationLoaded)
+ if (_configuration is not null)
{
return _configuration;
}
lock (_configurationSyncLock)
{
- if (_configurationLoaded)
+ if (_configuration is not null)
{
return _configuration;
}
- _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
-
- _configurationLoaded = true;
-
- return _configuration;
+ return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
}
}
protected set
{
_configuration = value;
-
- _configurationLoaded = value is not null;
}
}
@@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Saving system configuration");
var path = CommonApplicationPaths.SystemConfigurationFilePath;
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock)
{
@@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
private object LoadConfiguration(string path, Type configurationType)
{
- if (!File.Exists(path))
- {
- return Activator.CreateInstance(configurationType);
- }
-
try
{
- return XmlSerializer.DeserializeFromFile(configurationType, path);
- }
- catch (IOException)
- {
- return Activator.CreateInstance(configurationType);
+ if (File.Exists(path))
+ {
+ return XmlSerializer.DeserializeFromFile(configurationType, path);
+ }
}
- catch (Exception ex)
+ catch (Exception ex) when (ex is not IOException)
{
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
-
- return Activator.CreateInstance(configurationType);
}
+
+ return Activator.CreateInstance(configurationType)
+ ?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
}
/// <inheritdoc />
@@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
var path = GetConfigurationFile(key);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock)
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7b3d07dfc..080c44829 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -11,7 +11,6 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
-using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@@ -19,7 +18,6 @@ using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Naming.Common;
-using Emby.Notifications;
using Emby.Photos;
using Emby.Server.Implementations.Channels;
using Emby.Server.Implementations.Collections;
@@ -70,7 +68,6 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Notifications;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Plugins;
@@ -118,14 +115,10 @@ namespace Emby.Server.Implementations
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
{
/// <summary>
- /// The environment variable prefixes to log at server startup.
- /// </summary>
- private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
-
- /// <summary>
/// The disposable parts.
/// </summary>
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
+ private readonly DeviceId _deviceId;
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
@@ -134,7 +127,6 @@ namespace Emby.Server.Implementations
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
- private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
/// <summary>
@@ -143,8 +135,6 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
- private DeviceId _deviceId;
-
private bool _disposed = false;
/// <summary>
@@ -168,6 +158,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
+ _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -193,30 +184,11 @@ namespace Emby.Server.Implementations
/// </summary>
private string PublishedServerUrl => _startupConfig[AddressOverrideKey];
- /// <summary>
- /// Gets a value indicating whether this instance can self restart.
- /// </summary>
- public bool CanSelfRestart => _startupOptions.RestartPath is not null;
-
public bool CoreStartupHasCompleted { get; private set; }
- public virtual bool CanLaunchWebBrowser
- {
- get
- {
- if (!Environment.UserInteractive)
- {
- return false;
- }
-
- if (_startupOptions.IsService)
- {
- return false;
- }
-
- return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
- }
- }
+ public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
+ && !_startupOptions.IsService
+ && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
@@ -293,15 +265,7 @@ namespace Emby.Server.Implementations
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
- public string SystemId
- {
- get
- {
- _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
-
- return _deviceId.Value;
- }
- }
+ public string SystemId => _deviceId.Value;
/// <inheritdoc/>
public string Name => ApplicationProductName;
@@ -454,7 +418,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
- _mediaEncoder.SetFFmpegPath();
+ Resolve<IMediaEncoder>().SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId);
@@ -624,8 +588,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
-
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
@@ -654,7 +616,7 @@ namespace Emby.Server.Implementations
/// <returns>A task representing the service initialization operation.</returns>
public async Task InitializeServices()
{
- var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
+ var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
await using (jellyfinDb.ConfigureAwait(false))
{
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
@@ -668,7 +630,6 @@ namespace Emby.Server.Implementations
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
- _mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
SetStaticProperties();
@@ -679,36 +640,6 @@ namespace Emby.Server.Implementations
FindParts();
}
- public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
- {
- // Distinct these to prevent users from reporting problems that aren't actually problems
- var commandLineArgs = Environment
- .GetCommandLineArgs()
- .Distinct();
-
- // Get all relevant environment variables
- var allEnvVars = Environment.GetEnvironmentVariables();
- var relevantEnvVars = new Dictionary<object, object>();
- foreach (var key in allEnvVars.Keys)
- {
- if (_relevantEnvVarPrefixes.Any(prefix => key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
- {
- relevantEnvVars.Add(key, allEnvVars[key]);
- }
- }
-
- logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
- logger.LogInformation("Arguments: {Args}", commandLineArgs);
- logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
- logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
- logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
- logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
- logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
- logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
- logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
- logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
- }
-
private X509Certificate2 GetCertificate(string path, string password)
{
if (string.IsNullOrWhiteSpace(path))
@@ -795,13 +726,7 @@ namespace Emby.Server.Implementations
Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
- Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
-
- Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
-
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
-
- Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
}
/// <summary>
@@ -935,17 +860,13 @@ namespace Emby.Server.Implementations
/// </summary>
public void Restart()
{
- if (!CanSelfRestart)
- {
- throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually.");
- }
-
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
+ _pluginManager.UnloadAssemblies();
Task.Run(async () =>
{
@@ -1004,9 +925,6 @@ namespace Emby.Server.Implementations
// Local metadata
yield return typeof(BoxSetXmlSaver).Assembly;
- // Notifications
- yield return typeof(NotificationManager).Assembly;
-
// Xbmc
yield return typeof(ArtistNfoProvider).Assembly;
@@ -1045,15 +963,11 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
- OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
- OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
- CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
- SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
};
}
@@ -1065,7 +979,6 @@ namespace Emby.Server.Implementations
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
- OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
@@ -1275,10 +1188,13 @@ namespace Emby.Server.Implementations
}
}
- // used for closing websockets
- foreach (var session in _sessionManager.Sessions)
+ if (_sessionManager != null)
{
- await session.DisposeAsync().ConfigureAwait(false);
+ // used for closing websockets
+ foreach (var session in _sessionManager.Sessions)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 85ccbc028..961e225e9 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -66,6 +66,7 @@ namespace Emby.Server.Implementations.Channels
/// <param name="userDataManager">The user data manager.</param>
/// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param>
+ /// <param name="channels">The channels.</param>
public ChannelManager(
IUserManager userManager,
IDtoService dtoService,
@@ -75,7 +76,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem,
IUserDataManager userDataManager,
IProviderManager providerManager,
- IMemoryCache memoryCache)
+ IMemoryCache memoryCache,
+ IEnumerable<IChannel> channels)
{
_userManager = userManager;
_dtoService = dtoService;
@@ -86,19 +88,14 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager;
_providerManager = providerManager;
_memoryCache = memoryCache;
+ Channels = channels.ToArray();
}
- internal IChannel[] Channels { get; private set; }
+ internal IChannel[] Channels { get; }
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
/// <inheritdoc />
- public void AddParts(IEnumerable<IChannel> channels)
- {
- Channels = channels.ToArray();
- }
-
- /// <inheritdoc />
public bool EnableMediaSourceDisplay(BaseItem item)
{
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
@@ -160,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
- public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
+ public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
- var channels = GetAllChannels()
- .Select(GetChannelEntity)
+ var channels = await GetAllChannelEntitiesAsync()
.OrderBy(i => i.SortName)
- .ToList();
+ .ToListAsync()
+ .ConfigureAwait(false);
if (query.IsRecordingsFolder.HasValue)
{
@@ -229,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
if (user is not null)
{
+ var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
channels = channels.Where(i =>
{
if (!i.IsVisible(user))
@@ -238,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
try
{
- return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
+ return GetChannelProvider(i).IsEnabledFor(userId);
}
catch
{
@@ -261,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
{
foreach (var item in all)
{
- RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
+ await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
}
}
@@ -272,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
- public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
+ public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
- var internalResult = GetChannelsInternal(query);
+ var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
var dtoOptions = new DtoOptions();
@@ -330,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
progress.Report(100);
}
- private Channel GetChannelEntity(IChannel channel)
+ private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
{
- return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
+ foreach (IChannel channel in GetAllChannels())
+ {
+ yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
+ }
}
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
@@ -404,7 +405,7 @@ namespace Emby.Server.Implementations.Channels
}
else
{
- results = new List<MediaSourceInfo>();
+ results = Enumerable.Empty<MediaSourceInfo>();
}
return results
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index b53c8ca51..179683055 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -206,8 +206,7 @@ namespace Emby.Server.Implementations.Collections
throw new ArgumentException("No collection exists with the supplied Id");
}
- var list = new List<LinkedChild>();
- var itemList = new List<BaseItem>();
+ List<BaseItem>? itemList = null;
var linkedChildrenList = collection.GetLinkedChildren();
var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
@@ -223,18 +222,23 @@ namespace Emby.Server.Implementations.Collections
if (!currentLinkedChildrenIds.Contains(id))
{
- itemList.Add(item);
+ (itemList ??= new()).Add(item);
- list.Add(LinkedChild.Create(item));
linkedChildrenList.Add(item);
}
}
- if (list.Count > 0)
+ if (itemList is not null)
{
- LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
+ var originalLen = collection.LinkedChildren.Length;
+ var newItemCount = itemList.Count;
+ LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
collection.LinkedChildren.CopyTo(newChildren, 0);
- list.CopyTo(newChildren, collection.LinkedChildren.Length);
+ for (int i = 0; i < newItemCount; i++)
+ {
+ newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
+ }
+
collection.LinkedChildren = newChildren;
collection.UpdateRatingToItems(linkedChildrenList);
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index ff5602f24..6b8b1a620 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Globalization;
using System.IO;
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration
/// <summary>
/// Configuration updating event.
/// </summary>
- public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating;
+ public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating;
/// <summary>
/// Gets the type of the configuration.
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index bc703fe90..3bf4d07c5 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -586,7 +586,7 @@ namespace Emby.Server.Implementations.Data
/// <exception cref="ArgumentNullException">
/// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
/// </exception>
- public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
+ public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(items);
@@ -594,9 +594,11 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- var tuples = new List<(BaseItem, List<Guid>, BaseItem, string, List<string>)>();
- foreach (var item in items)
+ var itemsLen = items.Count;
+ var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen];
+ for (int i = 0; i < itemsLen; i++)
{
+ var item = items[i];
var ancestorIds = item.SupportsAncestors ?
item.GetAncestorIds().Distinct().ToList() :
null;
@@ -606,7 +608,7 @@ namespace Emby.Server.Implementations.Data
var userdataKey = item.GetUserDataKeys().FirstOrDefault();
var inheritedTags = item.GetInheritedTags();
- tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
+ tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
}
using (var connection = GetConnection())
@@ -1195,7 +1197,7 @@ namespace Emby.Server.Implementations.Data
Path = RestorePath(path.ToString())
};
- if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)
+ if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
&& ticks >= DateTime.MinValue.Ticks
&& ticks <= DateTime.MaxValue.Ticks)
{
@@ -3202,7 +3204,8 @@ namespace Emby.Server.Implementations.Data
return IsAlphaNumeric(value);
}
- private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement)
+#nullable enable
+ private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement)
{
if (query.IsResumable ?? false)
{
@@ -3677,7 +3680,6 @@ namespace Emby.Server.Implementations.Data
if (statement is not null)
{
nameContains = FixUnicodeChars(nameContains);
-
statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
}
}
@@ -3803,13 +3805,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ArtistIds)
{
var paramName = "@ArtistIds" + index;
-
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
- if (statement is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3824,13 +3821,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.AlbumArtistIds)
{
var paramName = "@ArtistIds" + index;
-
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
- if (statement is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3845,13 +3837,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ContributingArtistIds)
{
var paramName = "@ArtistIds" + index;
-
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
- if (statement is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3866,13 +3853,8 @@ namespace Emby.Server.Implementations.Data
foreach (var albumId in query.AlbumIds)
{
var paramName = "@AlbumIds" + index;
-
clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
- if (statement is not null)
- {
- statement.TryBind(paramName, albumId);
- }
-
+ statement?.TryBind(paramName, albumId);
index++;
}
@@ -3887,13 +3869,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ExcludeArtistIds)
{
var paramName = "@ExcludeArtistId" + index;
-
clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
- if (statement is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3908,13 +3885,8 @@ namespace Emby.Server.Implementations.Data
foreach (var genreId in query.GenreIds)
{
var paramName = "@GenreId" + index;
-
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
- if (statement is not null)
- {
- statement.TryBind(paramName, genreId);
- }
-
+ statement?.TryBind(paramName, genreId);
index++;
}
@@ -3929,11 +3901,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in query.Genres)
{
clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
- if (statement is not null)
- {
- statement.TryBind("@Genre" + index, GetCleanValue(item));
- }
-
+ statement?.TryBind("@Genre" + index, GetCleanValue(item));
index++;
}
@@ -3948,11 +3916,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in tags)
{
clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
- if (statement is not null)
- {
- statement.TryBind("@Tag" + index, GetCleanValue(item));
- }
-
+ statement?.TryBind("@Tag" + index, GetCleanValue(item));
index++;
}
@@ -3967,11 +3931,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in excludeTags)
{
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
- if (statement is not null)
- {
- statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
- }
-
+ statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item));
index++;
}
@@ -3986,14 +3946,8 @@ namespace Emby.Server.Implementations.Data
foreach (var studioId in query.StudioIds)
{
var paramName = "@StudioId" + index;
-
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
-
- if (statement is not null)
- {
- statement.TryBind(paramName, studioId);
- }
-
+ statement?.TryBind(paramName, studioId);
index++;
}
@@ -4008,11 +3962,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in query.OfficialRatings)
{
clauses.Add("OfficialRating=@OfficialRating" + index);
- if (statement is not null)
- {
- statement.TryBind("@OfficialRating" + index, item);
- }
-
+ statement?.TryBind("@OfficialRating" + index, item);
index++;
}
@@ -4020,35 +3970,97 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause);
}
- if (query.MinParentalRating.HasValue)
+ var ratingClauseBuilder = new StringBuilder("(");
+ if (query.HasParentalRating ?? false)
{
- whereClauses.Add("InheritedParentalRatingValue>=@MinParentalRating");
- if (statement is not null)
+ ratingClauseBuilder.Append("InheritedParentalRatingValue not null");
+ if (query.MinParentalRating.HasValue)
{
- statement.TryBind("@MinParentalRating", query.MinParentalRating.Value);
+ ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
+ statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
- }
- if (query.MaxParentalRating.HasValue)
+ if (query.MaxParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
+ statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
+ }
+ }
+ else if (query.BlockUnratedItems.Length > 0)
{
- whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating");
+ var paramName = "@UnratedType";
+ var index = 0;
+ string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++));
+ ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))");
+
if (statement is not null)
{
- statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
+ for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++)
+ {
+ statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString());
+ }
+ }
+
+ if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append(" OR (");
}
- }
- if (query.HasParentalRating.HasValue)
- {
- if (query.HasParentalRating.Value)
+ if (query.MinParentalRating.HasValue)
{
- whereClauses.Add("InheritedParentalRatingValue > 0");
+ ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
+ statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
- else
+
+ if (query.MaxParentalRating.HasValue)
{
- whereClauses.Add("InheritedParentalRatingValue = 0");
+ if (query.MinParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append(" AND ");
+ }
+
+ ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
+ statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
+ }
+
+ if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append(")");
+ }
+
+ if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
+ {
+ ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null");
}
}
+ else if (query.MinParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
+ statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
+
+ if (query.MaxParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
+ statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
+ }
+
+ ratingClauseBuilder.Append(")");
+ }
+ else if (query.MaxParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
+ statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
+ }
+ else if (!query.HasParentalRating ?? false)
+ {
+ ratingClauseBuilder.Append("InheritedParentalRatingValue is null");
+ }
+
+ var ratingClauseString = ratingClauseBuilder.ToString();
+ if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase))
+ {
+ whereClauses.Add(ratingClauseString + ")");
+ }
if (query.HasOfficialRating.HasValue)
{
@@ -4089,37 +4101,25 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)");
- if (statement is not null)
- {
- statement.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
- }
+ statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)");
- if (statement is not null)
- {
- statement.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
- }
+ statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)");
- if (statement is not null)
- {
- statement.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
- }
+ statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)");
- if (statement is not null)
- {
- statement.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
- }
+ statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
}
if (query.HasSubtitles.HasValue)
@@ -4169,15 +4169,11 @@ namespace Emby.Server.Implementations.Data
if (query.Years.Length == 1)
{
whereClauses.Add("ProductionYear=@Years");
- if (statement is not null)
- {
- statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
- }
+ statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
}
else if (query.Years.Length > 1)
{
var val = string.Join(',', query.Years);
-
whereClauses.Add("ProductionYear in (" + val + ")");
}
@@ -4185,10 +4181,7 @@ namespace Emby.Server.Implementations.Data
if (isVirtualItem.HasValue)
{
whereClauses.Add("IsVirtualItem=@IsVirtualItem");
- if (statement is not null)
- {
- statement.TryBind("@IsVirtualItem", isVirtualItem.Value);
- }
+ statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
}
if (query.IsSpecialSeason.HasValue)
@@ -4219,31 +4212,22 @@ namespace Emby.Server.Implementations.Data
if (queryMediaTypes.Length == 1)
{
whereClauses.Add("MediaType=@MediaTypes");
- if (statement is not null)
- {
- statement.TryBind("@MediaTypes", queryMediaTypes[0]);
- }
+ statement?.TryBind("@MediaTypes", queryMediaTypes[0]);
}
else if (queryMediaTypes.Length > 1)
{
var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
-
whereClauses.Add("MediaType in (" + val + ")");
}
if (query.ItemIds.Length > 0)
{
var includeIds = new List<string>();
-
var index = 0;
foreach (var id in query.ItemIds)
{
includeIds.Add("Guid = @IncludeId" + index);
- if (statement is not null)
- {
- statement.TryBind("@IncludeId" + index, id);
- }
-
+ statement?.TryBind("@IncludeId" + index, id);
index++;
}
@@ -4253,16 +4237,11 @@ namespace Emby.Server.Implementations.Data
if (query.ExcludeItemIds.Length > 0)
{
var excludeIds = new List<string>();
-
var index = 0;
foreach (var id in query.ExcludeItemIds)
{
excludeIds.Add("Guid <> @ExcludeId" + index);
- if (statement is not null)
- {
- statement.TryBind("@ExcludeId" + index, id);
- }
-
+ statement?.TryBind("@ExcludeId" + index, id);
index++;
}
@@ -4283,11 +4262,7 @@ namespace Emby.Server.Implementations.Data
var paramName = "@ExcludeProviderId" + index;
excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")");
- if (statement is not null)
- {
- statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- }
-
+ statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
index++;
break;
@@ -4312,7 +4287,7 @@ namespace Emby.Server.Implementations.Data
}
// TODO this seems to be an idea for a better schema where ProviderIds are their own table
- // buut this is not implemented
+ // but this is not implemented
// hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
// TODO this is a really BAD way to do it since the pair:
@@ -4326,11 +4301,7 @@ namespace Emby.Server.Implementations.Data
hasProviderIds.Add("ProviderIds like " + paramName);
// this replaces the placeholder with a value, here: %key=val%
- if (statement is not null)
- {
- statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- }
-
+ statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
index++;
break;
@@ -4407,11 +4378,7 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length == 1)
{
whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
-
- if (statement is not null)
- {
- statement.TryBind("@AncestorId", query.AncestorIds[0]);
- }
+ statement?.TryBind("@AncestorId", query.AncestorIds[0]);
}
if (query.AncestorIds.Length > 1)
@@ -4424,39 +4391,13 @@ namespace Emby.Server.Implementations.Data
{
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
- if (statement is not null)
- {
- statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
- }
+ statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
}
if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
{
whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
-
- if (statement is not null)
- {
- statement.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
- }
- }
-
- if (query.BlockUnratedItems.Length == 1)
- {
- whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)");
- if (statement is not null)
- {
- statement.TryBind("@UnratedType", query.BlockUnratedItems[0].ToString());
- }
- }
-
- if (query.BlockUnratedItems.Length > 1)
- {
- var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
- whereClauses.Add(
- string.Format(
- CultureInfo.InvariantCulture,
- "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
- inClause));
+ statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
}
if (query.ExcludeInheritedTags.Length > 0)
@@ -4477,6 +4418,24 @@ namespace Emby.Server.Implementations.Data
}
}
+ if (query.IncludeInheritedTags.Length > 0)
+ {
+ var paramName = "@IncludeInheritedTags";
+ if (statement is null)
+ {
+ int index = 0;
+ string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
+ whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
+ }
+ else
+ {
+ for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
+ {
+ statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
+ }
+ }
+ }
+
if (query.SeriesStatuses.Length > 0)
{
var statuses = new List<string>();
@@ -4587,6 +4546,7 @@ namespace Emby.Server.Implementations.Data
return whereClauses;
}
+#nullable disable
/// <summary>
/// Formats a where clause for the specified provider.
@@ -5440,6 +5400,9 @@ AND Type = @InternalPersonType)");
list.AddRange(inheritedTags.Select(i => (6, i)));
+ // Remove all invalid values.
+ list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
+
return list;
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 5103b1fbf..45270de89 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -83,22 +83,23 @@ namespace Emby.Server.Implementations.Dto
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
- var returnItems = new BaseItemDto[items.Count];
- var programTuples = new List<(BaseItem, BaseItemDto)>();
- var channelTuples = new List<(BaseItemDto, LiveTvChannel)>();
+ var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
+ var returnItems = new BaseItemDto[accessibleItems.Count];
+ List<(BaseItem, BaseItemDto)> programTuples = null;
+ List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
- for (int index = 0; index < items.Count; index++)
+ for (int index = 0; index < accessibleItems.Count; index++)
{
- var item = items[index];
+ var item = accessibleItems[index];
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
{
- channelTuples.Add((dto, tvChannel));
+ (channelTuples ??= new()).Add((dto, tvChannel));
}
else if (item is LiveTvProgram)
{
- programTuples.Add((item, dto));
+ (programTuples ??= new()).Add((item, dto));
}
if (item is IItemByName byName)
@@ -121,12 +122,12 @@ namespace Emby.Server.Implementations.Dto
returnItems[index] = dto;
}
- if (programTuples.Count > 0)
+ if (programTuples is not null)
{
LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
}
- if (channelTuples.Count > 0)
+ if (channelTuples is not null)
{
LivetvManager.AddChannelInfo(channelTuples, options, user);
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 7accc3b8b..b8655c760 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -7,7 +7,6 @@
<ItemGroup>
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
- <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
@@ -23,17 +22,17 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
- <PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
- <PackageReference Include="Mono.Nat" Version="3.0.4" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
- <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
- <PackageReference Include="DotNet.Glob" Version="3.1.3" />
+ <PackageReference Include="DiscUtils.Udf" />
+ <PackageReference Include="Jellyfin.XmlTv" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
+ <PackageReference Include="Mono.Nat" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" />
+ <PackageReference Include="SQLitePCL.pretty.netstandard" />
+ <PackageReference Include="DotNet.Glob" />
</ItemGroup>
<ItemGroup>
@@ -54,13 +53,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 05d0a9b79..2e3988f9e 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -276,25 +276,31 @@ namespace Emby.Server.Implementations.EntryPoints
/// Libraries the update timer callback.
/// </summary>
/// <param name="state">The state.</param>
- private void LibraryUpdateTimerCallback(object state)
+ private async void LibraryUpdateTimerCallback(object state)
{
+ List<Folder> foldersAddedTo;
+ List<Folder> foldersRemovedFrom;
+ List<BaseItem> itemsUpdated;
+ List<BaseItem> itemsAdded;
+ List<BaseItem> itemsRemoved;
lock (_libraryChangedSyncLock)
{
// Remove dupes in case some were saved multiple times
- var foldersAddedTo = _foldersAddedTo
+ foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
- var foldersRemovedFrom = _foldersRemovedFrom
+ foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
- var itemsUpdated = _itemsUpdated
+ itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.DistinctBy(x => x.Id)
.ToList();
- SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
+ itemsAdded = _itemsAdded.ToList();
+ itemsRemoved = _itemsRemoved.ToList();
if (LibraryUpdateTimer is not null)
{
@@ -308,6 +314,8 @@ namespace Emby.Server.Implementations.EntryPoints
_foldersAddedTo.Clear();
_foldersRemovedFrom.Clear();
}
+
+ await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index e724618b3..d32759017 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private void UpdateTimerCallback(object? state)
+ private async void UpdateTimerCallback(object? state)
{
+ List<KeyValuePair<Guid, List<BaseItem>>> changes;
lock (_syncLock)
{
// Remove dupes in case some were saved multiple times
- var changes = _changedItems.ToList();
+ changes = _changedItems.ToList();
_changedItems.Clear();
- SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
-
if (_updateTimer is not null)
{
_updateTimer.Dispose();
_updateTimer = null;
}
}
+
+ await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
}
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
{
- foreach (var pair in changes)
+ foreach ((var key, var value) in changes)
{
- await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
+ await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 3769ae4dd..b7bcaace1 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -21,16 +21,6 @@ namespace Emby.Server.Implementations
string? PackageName { get; }
/// <summary>
- /// Gets the value of the --restartpath command line option.
- /// </summary>
- string? RestartPath { get; }
-
- /// <summary>
- /// Gets the value of the --restartargs command line option.
- /// </summary>
- string? RestartArgs { get; }
-
- /// <summary>
/// Gets the value of the --published-server-url command line option.
/// </summary>
string? PublishedServerUrl { get; }
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 6fc7f1ac3..84c21931c 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 4376bd356..90f7568a9 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 968bf5fa3..c9b41f819 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a3c66dc79..e5c520ca2 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param>
+ /// <param name="directoryService">The directory service.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository,
IImageProcessor imageProcessor,
IMemoryCache memoryCache,
- NamingOptions namingOptions)
+ NamingOptions namingOptions,
+ IDirectoryService directoryService)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library
_memoryCache = memoryCache;
_namingOptions = namingOptions;
- _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions);
+ _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -356,8 +358,8 @@ namespace Emby.Server.Implementations.Library
}
var children = item.IsFolder
- ? ((Folder)item).GetRecursiveChildren(false).ToList()
- : new List<BaseItem>();
+ ? ((Folder)item).GetRecursiveChildren(false)
+ : Enumerable.Empty<BaseItem>();
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
collectionType = GetContentTypeOverride(fullPath, true);
}
- var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
{
Parent = parent,
FileInfo = fileInfo,
@@ -1253,7 +1255,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ SetTopParentIdsOrAncestors(query, new[] { parent });
}
}
@@ -1277,7 +1279,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ SetTopParentIdsOrAncestors(query, new[] { parent });
}
}
@@ -1435,7 +1437,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ SetTopParentIdsOrAncestors(query, new[] { parent });
}
}
@@ -1455,7 +1457,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.GetItemList(query));
}
- private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents)
+ private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
{
if (parents.All(i => i is ICollectionFolder || i is UserView))
{
@@ -1602,7 +1604,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error getting intros");
- return new List<IntroInfo>();
+ return Enumerable.Empty<IntroInfo>();
}
}
@@ -2876,7 +2878,7 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
- var personsToSave = new List<BaseItem>();
+ List<BaseItem> personsToSave = null;
foreach (var person in people)
{
@@ -2918,12 +2920,12 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
- personsToSave.Add(personEntity);
+ (personsToSave ??= new()).Add(personEntity);
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
}
}
- if (personsToSave.Count > 0)
+ if (personsToSave is not null)
{
CreateItems(personsToSave, null, CancellationToken.None);
}
@@ -3085,22 +3087,19 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(path));
}
- var removeList = new List<NameValuePair>();
+ List<NameValuePair> removeList = null;
foreach (var contentType in _configurationManager.Configuration.ContentTypes)
{
- if (string.IsNullOrWhiteSpace(contentType.Name))
- {
- removeList.Add(contentType);
- }
- else if (_fileSystem.AreEqual(path, contentType.Name)
+ if (string.IsNullOrWhiteSpace(contentType.Name)
+ || _fileSystem.AreEqual(path, contentType.Name)
|| _fileSystem.ContainsSubPath(path, contentType.Name))
{
- removeList.Add(contentType);
+ (removeList ??= new()).Add(contentType);
}
}
- if (removeList.Count > 0)
+ if (removeList is not null)
{
_configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
.Except(removeList)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 06621700a..a74f82475 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -158,7 +158,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
{
var files = new List<FileSystemMetadata>();
- var items = new List<BaseItem>();
var leftOver = new List<FileSystemMetadata>();
// Loop through each child file/folder and see if we find a video
@@ -180,7 +179,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var result = new MultiItemResolverResult
{
ExtraFiles = leftOver,
- Items = items
+ Items = new List<BaseItem>()
};
var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
@@ -193,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
- if (resolvedItem.Files.Count == 0)
+ // Until multi-part books are handled letting files stack hides them from browsing in the client
+ if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0)
{
continue;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index a922e3685..bbc70701c 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
private readonly ILogger<MusicAlbumResolver> _logger;
private readonly NamingOptions _namingOptions;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_logger = logger;
_namingOptions = namingOptions;
+ _directoryService = directoryService;
}
/// <summary>
@@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
// If args contains music it's a music album
- if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
+ if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
{
return true;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 2538c2b5b..c858dc53d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicArtistResolver : ItemResolver<MusicArtist>
{
private readonly ILogger<MusicAlbumResolver> _logger;
- private NamingOptions _namingOptions;
+ private readonly NamingOptions _namingOptions;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
+ /// <param name="directoryService">The directory service.</param>
public MusicArtistResolver(
ILogger<MusicAlbumResolver> logger,
- NamingOptions namingOptions)
+ NamingOptions namingOptions,
+ IDirectoryService directoryService)
{
_logger = logger;
_namingOptions = namingOptions;
+ _directoryService = directoryService;
}
/// <summary>
@@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null;
}
- var directoryService = args.DirectoryService;
-
- var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
+ var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
@@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
// If we contain a music album assume we are an artist folder
- if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+ if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
{
// Stop once we see a music album
state.Stop();
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index e8615e7db..4fac91bf1 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
private readonly ILogger _logger;
- protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
+ protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_logger = logger;
NamingOptions = namingOptions;
+ DirectoryService = directoryService;
}
protected NamingOptions NamingOptions { get; }
+ protected IDirectoryService DirectoryService { get; }
+
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -65,13 +68,25 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = child.Name;
if (child.IsDirectory)
{
- if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
+ if (IsDvdDirectory(child.FullName, filename, DirectoryService))
{
- videoType = VideoType.Dvd;
+ var videoTmp = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd
+ };
+ Set3DFormat(videoTmp);
+ return videoTmp;
}
else if (IsBluRayDirectory(filename))
{
- videoType = VideoType.BluRay;
+ var videoTmp = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay
+ };
+ Set3DFormat(videoTmp);
+ return videoTmp;
}
}
else if (IsDvdFile(filename))
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index 30c52e19d..0b255f673 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -4,6 +4,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -25,11 +26,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
- public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_namingOptions = namingOptions;
- _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) };
- _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) };
+ _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
+ _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions, directoryService) };
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
index 5e33b402d..ba320266a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
@@ -2,6 +2,7 @@
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers
@@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public GenericVideoResolver(ILogger logger, NamingOptions namingOptions)
- : base(logger, namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ : base(logger, namingOptions, directoryService)
{
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 1522cd3ae..ea980b992 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions)
- : base(logger, namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ : base(logger, namingOptions, directoryService)
{
_imageProcessor = imageProcessor;
}
@@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
- movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
- movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
- movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
// ignore extras
@@ -313,13 +314,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
- private static bool IsIgnored(string filename)
- {
- // Ignore samples
- Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
-
- return m.Success;
- }
+ private static bool IsIgnored(ReadOnlySpan<char> filename)
+ => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index e11fb262e..9026160ff 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -12,15 +10,20 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
{
+ /// <summary>
+ /// Class PhotoResolver.
+ /// </summary>
public class PhotoResolver : ItemResolver<Photo>
{
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
+ private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
@@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
"default"
};
- public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PhotoResolver"/> class.
+ /// </summary>
+ /// <param name="imageProcessor">The image processor.</param>
+ /// <param name="namingOptions">The naming options.</param>
+ /// <param name="directoryService">The directory service.</param>
+ public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService)
{
_imageProcessor = imageProcessor;
_namingOptions = namingOptions;
+ _directoryService = directoryService;
}
/// <summary>
@@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = Path.GetFileNameWithoutExtension(args.Path);
// Make sure the image doesn't belong to a video file
- var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
+ var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
foreach (var file in files)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 0fcc5070b..392ee4c77 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions)
- : base(logger, namingOptions)
+ /// <param name="directoryService">The directory service.</param>
+ public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ : base(logger, namingOptions, directoryService)
{
}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 1137625f4..17f1d1905 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -111,10 +111,10 @@ namespace Emby.Server.Implementations.Library
if (query.IncludeExternalContent)
{
- var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
+ var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
{
UserId = query.UserId
- });
+ }).GetAwaiter().GetResult();
var channels = channelResult.Items;
@@ -286,7 +286,7 @@ namespace Emby.Server.Implementations.Library
if (parents.Count == 0)
{
- return new List<BaseItem>();
+ return Array.Empty<BaseItem>();
}
if (includeItemTypes.Length == 0)
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 3f7914d3b..b5e742f98 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -570,15 +570,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_tokens.TryAdd(username, savedToken);
}
- if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value))
+ if (!string.IsNullOrEmpty(savedToken.Name)
+ && long.TryParse(savedToken.Value, CultureInfo.InvariantCulture, out long ticks))
{
- if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long ticks))
+ // If it's under 24 hours old we can still use it
+ if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
{
- // If it's under 24 hours old we can still use it
- if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
- {
- return savedToken.Name;
- }
+ return savedToken.Name;
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index e874990da..066afb956 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
{
- string episodeTitle = program.Episode?.Title;
+ string episodeTitle = program.Episode.Title;
+ var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
var programInfo = new ProgramInfo
{
ChannelId = program.ChannelId,
EndDate = program.EndDate.UtcDateTime,
- EpisodeNumber = program.Episode?.Episode,
+ EpisodeNumber = program.Episode.Episode,
EpisodeTitle = episodeTitle,
- Genres = program.Categories,
+ Genres = programCategories,
StartDate = program.StartDate.UtcDateTime,
Name = program.Title,
Overview = program.Description,
ProductionYear = program.CopyrightDate?.Year,
- SeasonNumber = program.Episode?.Series,
- IsSeries = program.Episode is not null,
+ SeasonNumber = program.Episode.Series,
+ IsSeries = program.Episode.Series is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not null,
- IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
- IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
- IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
- IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
CommunityRating = program.StarRating,
- SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+ SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
if (string.IsNullOrWhiteSpace(program.ProgramId))
@@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
Id = c.Id,
Name = c.DisplayName,
- ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source,
+ ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
}).ToList();
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 4003468d0..ee039ff0f 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
return 7;
}
- private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
+ private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user)
{
if (user is null)
{
return new QueryResult<BaseItem>();
}
- var folderIds = GetRecordingFolders(user, true)
- .Select(i => i.Id)
- .ToList();
+ var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false);
+ var folderIds = Array.ConvertAll(folders, x => x.Id);
var excludeItemTypes = new List<BaseItemKind>();
- if (folderIds.Count == 0)
+ if (folderIds.Length == 0)
{
return new QueryResult<BaseItem>();
}
@@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
{
MediaTypes = new[] { MediaType.Video },
Recursive = true,
- AncestorIds = folderIds.ToArray(),
+ AncestorIds = folderIds,
IsFolder = false,
IsVirtualItem = false,
Limit = limit,
@@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options)
+ public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options)
{
var user = query.UserId.Equals(default)
? null
@@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
RemoveFields(options);
- var internalResult = GetEmbyRecordings(query, options, user);
+ var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false);
var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
@@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
return _tvDtoService.GetInternalProgramId(externalId);
}
- public List<BaseItem> GetRecordingFolders(User user)
- {
- return GetRecordingFolders(user, false);
- }
+ /// <inheritdoc />
+ public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
+ => GetRecordingFoldersAsync(user, false);
- private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels)
+ private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels)
{
var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
.SelectMany(i => i.Locations)
@@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
.OrderBy(i => i.SortName)
.ToList();
- folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+ var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
{
UserId = user.Id,
IsRecordingsFolder = true,
RefreshLatestChannelItems = refreshChannels
- }).Items);
+ }).ConfigureAwait(false);
+
+ folders.AddRange(channels.Items);
- return folders.Cast<BaseItem>().ToList();
+ return folders.Cast<BaseItem>().ToArray();
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 5327b3d74..98bbc1540 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -14,6 +14,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -58,7 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_socketFactory = socketFactory;
_streamHelper = streamHelper;
- _jsonOptions = JsonDefaults.Options;
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
+ _jsonOptions.Converters.Add(new JsonBoolNumberConverter());
}
public string Name => "HD Homerun";
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
index 80d9d0724..3450f971f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
@@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public LegacyHdHomerunChannelCommands(string url)
{
// parse url for channel and program
- var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
- var match = regExp.Match(url);
+ var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
if (match.Success)
{
_channel = match.Groups[1].Value;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index a423ec8f4..f2020e05f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var attributes = ParseExtInf(extInf, out string remaining);
extInf = remaining;
- if (attributes.TryGetValue("tvg-logo", out string value))
+ if (attributes.TryGetValue("tvg-logo", out string tvgLogo))
{
- channel.ImageUrl = value;
+ channel.ImageUrl = tvgLogo;
+ }
+ else if (attributes.TryGetValue("logo", out string logo))
+ {
+ channel.ImageUrl = logo;
}
if (attributes.TryGetValue("group-title", out string groupTitle))
@@ -168,28 +172,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
string numberString = null;
string attributeValue;
- if (attributes.TryGetValue("tvg-chno", out attributeValue))
+ if (attributes.TryGetValue("tvg-chno", out attributeValue)
+ && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
- {
- numberString = attributeValue;
- }
+ numberString = attributeValue;
}
if (!IsValidChannelNumber(numberString))
{
if (attributes.TryGetValue("tvg-id", out attributeValue))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+ if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;
}
- else if (attributes.TryGetValue("channel-id", out attributeValue))
+ else if (attributes.TryGetValue("channel-id", out attributeValue)
+ && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
- {
- numberString = attributeValue;
- }
+ numberString = attributeValue;
}
}
@@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
- if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+ if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
{
numberString = numberPart.ToString();
}
@@ -255,19 +255,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private static bool IsValidChannelNumber(string numberString)
{
- if (string.IsNullOrWhiteSpace(numberString) ||
- string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+ if (string.IsNullOrWhiteSpace(numberString)
+ || string.Equals(numberString, "-1", StringComparison.Ordinal)
+ || string.Equals(numberString, "0", StringComparison.Ordinal))
{
return false;
}
- return true;
+ return double.TryParse(numberString, CultureInfo.InvariantCulture, out _);
}
private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
@@ -285,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
- if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+ if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
{
// channel.Number = number.ToString();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
@@ -317,8 +312,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
- var matches = reg.Matches(line);
+ var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
remaining = line;
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 56c4e7d39..3af124678 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -1,4 +1,127 @@
{
- "Sync": "Сінхранізацыя",
- "Playlists": "Плэйліст"
+ "Sync": "Сінхранізаваць",
+ "Playlists": "Плэйлісты",
+ "Latest": "Апошні",
+ "LabelIpAddressValue": "IP-адрас: {0}",
+ "ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
+ "MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
+ "NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана",
+ "PluginInstalledWithName": "{0} быў усталяваны",
+ "UserCreatedWithName": "Карыстальнік {0} быў створаны",
+ "Albums": "Альбомы",
+ "Application": "Прыкладанне",
+ "AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны",
+ "Channels": "Каналы",
+ "ChapterNameValue": "Раздзел {0}",
+ "Collections": "Калекцыі",
+ "Default": "Па змаўчанні",
+ "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
+ "Folders": "Папкі",
+ "Favorites": "Абранае",
+ "External": "Знешні",
+ "Genres": "Жанры",
+ "HeaderContinueWatching": "Працягнуць прагляд",
+ "HeaderFavoriteAlbums": "Абраныя альбомы",
+ "HeaderFavoriteEpisodes": "Абраныя серыі",
+ "HeaderFavoriteShows": "Абраныя шоу",
+ "HeaderFavoriteSongs": "Абраныя песні",
+ "HeaderLiveTV": "Прамы эфір",
+ "HeaderAlbumArtists": "Выканаўцы альбома",
+ "LabelRunningTimeValue": "Працягласць: {0}",
+ "HomeVideos": "Хатнія відэа",
+ "ItemRemovedWithName": "{0} быў выдалены з бібліятэкі",
+ "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}",
+ "Movies": "Фільмы",
+ "Music": "Музыка",
+ "MusicVideos": "Музычныя кліпы",
+ "NameInstallFailed": "Устаноўка {0} не атрымалася",
+ "NameSeasonNumber": "Сезон {0}",
+ "NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання",
+ "NotificationOptionPluginInstalled": "Плагін усталяваны",
+ "NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана",
+ "NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера",
+ "Photos": "Фатаграфіі",
+ "Plugin": "Плагін",
+ "PluginUninstalledWithName": "{0} быў выдалены",
+ "PluginUpdatedWithName": "{0} быў абноўлены",
+ "ProviderValue": "Пастаўшчык: {0}",
+ "Songs": "Песні",
+ "System": "Сістэма",
+ "User": "Карыстальнік",
+ "UserDeletedWithName": "Карыстальнік {0} быў выдалены",
+ "UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
+ "TaskOptimizeDatabase": "Аптымізаваць базу дадзеных",
+ "Artists": "Выканаўцы",
+ "UserOfflineFromDevice": "{0} адключыўся ад {1}",
+ "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
+ "TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.",
+ "TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
+ "TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
+ "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.",
+ "TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
+ "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.",
+ "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.",
+ "TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
+ "TasksApplicationCategory": "Прыкладанне",
+ "AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}",
+ "Books": "Кнігі",
+ "CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
+ "DeviceOfflineWithName": "{0} адключыўся",
+ "DeviceOnlineWithName": "{0} падлучаны",
+ "Forced": "Прымусова",
+ "HeaderRecordingGroups": "Групы запісаў",
+ "HeaderNextUp": "Наступнае",
+ "HeaderFavoriteArtists": "Абраныя выканаўцы",
+ "HearingImpaired": "Са слабым слыхам",
+ "Inherit": "Атрымаць у спадчыну",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена",
+ "MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена",
+ "MixedContent": "Змешаны змест",
+ "NameSeasonUnknown": "Невядомы сезон",
+ "NotificationOptionInstallationFailed": "Збой усталёўкі",
+ "NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
+ "NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
+ "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
+ "NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
+ "NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
+ "NotificationOptionPluginError": "Збой плагіна",
+ "NotificationOptionPluginUninstalled": "Плагін выдалены",
+ "NotificationOptionTaskFailed": "Збой запланаванага задання",
+ "NotificationOptionUserLockedOut": "Карыстальнік заблакіраваны",
+ "NotificationOptionVideoPlayback": "Пачалося прайграванне відэа",
+ "NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена",
+ "ScheduledTaskFailedWithName": "{0} не атрымалася",
+ "ScheduledTaskStartedWithName": "{0} пачалося",
+ "ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць",
+ "Shows": "Шоу",
+ "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
+ "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
+ "TvShows": "ТБ-шоу",
+ "Undefined": "Нявызначана",
+ "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
+ "UserOnlineFromDevice": "{0} падключаны з {1}",
+ "UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}",
+ "UserStartedPlayingItemWithValues": "{0} грае {1} на {2}",
+ "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
+ "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
+ "ValueSpecialEpisodeName": "Спецэпізод - {0}",
+ "VersionNumber": "Версія {0}",
+ "TasksMaintenanceCategory": "Абслугоўванне",
+ "TasksLibraryCategory": "Медыятэка",
+ "TasksChannelsCategory": "Інтэрнэт-каналы",
+ "TaskCleanActivityLog": "Ачысціць журнал актыўнасці",
+ "TaskCleanCache": "Ачысціць кэш",
+ "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
+ "TaskRefreshChapterImages": "Выняць выявы раздзелаў",
+ "TaskRefreshLibrary": "Сканіраваць медыятэку",
+ "TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
+ "TaskCleanLogs": "Ачысціць часопіс",
+ "TaskRefreshPeople": "Абнавіць людзей",
+ "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
+ "TaskUpdatePlugins": "Абнавіць плагіны",
+ "TaskCleanTranscode": "Ачысціць каталог перакадзіравання",
+ "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
+ "TaskRefreshChannels": "Абнавіць каналы",
+ "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
+ "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу."
}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 64cb36fd8..13b99cc99 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Оптимизирай базата данни",
"TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
"TaskKeyframeExtractor": "Извличане на ключови кадри",
- "External": "Външен"
+ "External": "Външен",
+ "HearingImpaired": "Увреден слух"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 8ad9e8c71..8bd3c5def 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -118,11 +118,11 @@
"TaskCleanActivityLog": "Borrar log de actividades",
"Undefined": "Indefinido",
"Forced": "Forzado",
- "Default": "Por Defecto",
+ "Default": "Predeterminado",
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
"TaskOptimizeDatabase": "Optimización de base de datos",
"External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
- "HearingImpaired": "Personas con discapacidad auditiva"
+ "HearingImpaired": "Discapacidad Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index afffdf3bf..5e41462db 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
- "Latest": "Últimos",
+ "Latest": "Último contenido en",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index d6078c9c6..3d5c04633 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimizar base de datos",
"External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
- "TaskKeyframeExtractor": "Extractor de Fotogramas Clave"
+ "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
+ "HearingImpaired": "Discapacidad auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 026648af4..8e4bba25b 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
- "External": "خارجی"
+ "External": "خارجی",
+ "HearingImpaired": "مشکل شنوایی"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index bd8cec710..ac9da1dd1 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -1,7 +1,7 @@
{
"Albums": "Alben",
"AppDeviceValues": "App: {0}, Gerät: {1}",
- "Application": "Anwendung",
+ "Application": "Applikation",
"Artists": "Künstler",
"AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
"Books": "Bücher",
@@ -14,7 +14,7 @@
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
"Favorites": "Favoriten",
"Folders": "Ordner",
- "Genres": "Genres",
+ "Genres": "Genre",
"HeaderAlbumArtists": "Album-Künstler",
"HeaderContinueWatching": "weiter schauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
@@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "Audiowedergab gstartet",
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
"NotificationOptionCameraImageUploaded": "Foti ueglade",
- "NotificationOptionInstallationFailed": "Installationsfehler",
+ "NotificationOptionInstallationFailed": "Installationsfähler",
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
"NotificationOptionPluginError": "Plugin-Fäuer",
"NotificationOptionPluginInstalled": "Plugin installiert",
@@ -120,5 +120,9 @@
"Forced": "Erzwungen",
"Default": "Standard",
"TaskOptimizeDatabase": "Datenbank optimieren",
- "External": "Extern"
+ "External": "Extern",
+ "TaskOptimizeDatabaseDescription": "Kompromiert d Datenbank und trennt freie Speicherplatz. Durch die Ufagb cha d Leistig nach em ne Scan vor Bibliothek oder andere Ufgabe verbesseret werde.",
+ "HearingImpaired": "Hörgschädigti",
+ "TaskKeyframeExtractor": "Keyframe-Extraktor",
+ "TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 182b43ffc..a0e2f04a1 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -67,5 +67,11 @@
"Plugin": "प्लग-इन",
"Playlists": "प्लेलिस्ट",
"Photos": "तस्वीरें",
- "External": "बाहरी"
+ "External": "बाहरी",
+ "PluginUpdatedWithName": "{0} अपडेट हुए",
+ "ScheduledTaskStartedWithName": "{0} शुरू हुए",
+ "Songs": "गाने",
+ "UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं",
+ "UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया",
+ "StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 695c0f404..87ce07da3 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -82,7 +82,7 @@
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
- "CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}",
+ "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
"DeviceOnlineWithName": "{0} telah terhubung",
"NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index b262a8b42..a40f49506 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -107,5 +107,14 @@
"TasksApplicationCategory": "Forrit",
"TasksLibraryCategory": "Miðlasafn",
"TasksMaintenanceCategory": "Viðhald",
- "Default": "Sjálfgefið"
+ "Default": "Sjálfgefið",
+ "TaskCleanActivityLog": "Hreinsa athafnaskrá",
+ "TaskRefreshPeople": "Endurnýja fólk",
+ "TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
+ "TaskOptimizeDatabase": "Fínstilla gagnagrunn",
+ "Undefined": "Óskilgreint",
+ "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
+ "TaskCleanLogs": "Hreinsa færslu skrá",
+ "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
+ "HearingImpaired": "Heyrnarskertur"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index e03747cbe..01a2ab273 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -58,8 +58,8 @@
"NotificationOptionServerRestartRequired": "Server herstart nodig",
"NotificationOptionTaskFailed": "Geplande taak mislukt",
"NotificationOptionUserLockedOut": "Gebruiker is vergrendeld",
- "NotificationOptionVideoPlayback": "Video gestart",
- "NotificationOptionVideoPlaybackStopped": "Video gestopt",
+ "NotificationOptionVideoPlayback": "Afspelen van video gestart",
+ "NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt",
"Photos": "Foto's",
"Playlists": "Afspeellijsten",
"Plugin": "Plug-in",
@@ -95,26 +95,26 @@
"TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
"TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
"TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
- "TaskRefreshChannels": "Vernieuw Kanalen",
+ "TaskRefreshChannels": "Kanalen vernieuwen",
"TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
"TaskCleanLogs": "Logboekmap opschonen",
"TaskCleanTranscode": "Transcoderingsmap opschonen",
"TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.",
"TaskUpdatePlugins": "Plug-ins bijwerken",
- "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.",
+ "TaskRefreshPeopleDescription": "Updatet metadata voor acteurs en regisseurs in je mediabibliotheek.",
"TaskRefreshPeople": "Personen vernieuwen",
"TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
"TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
"TaskRefreshLibrary": "Mediabibliotheek scannen",
- "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
- "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken",
+ "TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.",
+ "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren",
"TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
"TaskCleanCache": "Cache-map opschonen",
- "TasksChannelsCategory": "Internet Kanalen",
+ "TasksChannelsCategory": "Internetkanalen",
"TasksApplicationCategory": "Toepassing",
"TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud",
- "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
+ "TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 506c14fdc..87800a2fe 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -13,5 +13,16 @@
"DeviceOfflineWithName": "{0} abandoned ship",
"AppDeviceValues": "Captain: {0}, Ship: {1}",
"CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}",
- "Collections": "Barrels"
+ "Collections": "Barrels",
+ "ItemAddedWithName": "{0} is now with yer treasure",
+ "Default": "Normal-like",
+ "FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
+ "Favorites": "Finest Loot",
+ "ItemRemovedWithName": "{0} was taken from yer treasure",
+ "LabelIpAddressValue": "Ship's coordinates: {0}",
+ "Genres": "types o' booty",
+ "TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
+ "HeaderAlbumArtists": "Buccaneers o' the musical arts",
+ "HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
+ "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 39229f45f..92e0d34ae 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -121,5 +121,6 @@
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"External": "Externo",
- "HearingImpaired": "Problemas auditivos"
+ "HearingImpaired": "Problemas auditivos",
+ "TaskKeyframeExtractor": "Extrator de quadro-chave"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 65cf29e80..839bbcb6d 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -16,14 +16,14 @@
"Folders": "Папки",
"Genres": "Жанры",
"HeaderAlbumArtists": "Исполнители альбома",
- "HeaderContinueWatching": "Продолжение просмотра",
+ "HeaderContinueWatching": "Продолжить просмотр",
"HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители",
"HeaderFavoriteEpisodes": "Избранные эпизоды",
"HeaderFavoriteShows": "Избранные сериалы",
"HeaderFavoriteSongs": "Избранные композиции",
"HeaderLiveTV": "Эфир",
- "HeaderNextUp": "Очередное",
+ "HeaderNextUp": "Следующий",
"HeaderRecordingGroups": "Группы записей",
"HomeVideos": "Домашние видео",
"Inherit": "Наследуемое",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} - неудачна",
"ScheduledTaskStartedWithName": "{0} - запущена",
"ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}",
- "Shows": "Передачи",
+ "Shows": "Телешоу",
"Songs": "Композиции",
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index d845accac..4c23f71ef 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimiziraj bazo podatkov",
"TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
"External": "Zunanji",
- "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa."
+ "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.",
+ "HearingImpaired": "Oslabljen sluh"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 318a0f3cf..785e6b226 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -66,7 +66,7 @@
"PluginInstalledWithName": "{0} installerades",
"PluginUninstalledWithName": "{0} avinstallerades",
"PluginUpdatedWithName": "{0} uppdaterades",
- "ProviderValue": "Källa: {0}",
+ "ProviderValue": "Leverantör: {0}",
"ScheduledTaskFailedWithName": "{0} misslyckades",
"ScheduledTaskStartedWithName": "{0} startades",
"ServerNameNeedsToBeRestarted": "{0} behöver startas om",
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 92ce616f2..ff77fb8c5 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -86,7 +86,7 @@
"Shows": "Шоу",
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
"ScheduledTaskStartedWithName": "{0} розпочато",
- "ScheduledTaskFailedWithName": "Помилка {0}",
+ "ScheduledTaskFailedWithName": "{0} незавершено, збій",
"ProviderValue": "Постачальник: {0}",
"PluginUpdatedWithName": "{0} оновлено",
"PluginUninstalledWithName": "{0} видалено",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index baa9ecc1c..cdc25ec7c 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -9,15 +9,15 @@
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "合輯",
- "DeviceOfflineWithName": "{0} 已經斷開連結",
+ "DeviceOfflineWithName": "{0} 已經斷開連接",
"DeviceOnlineWithName": "{0} 已經連接",
- "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗",
+ "FailedLoginAttemptWithUserName": "{0} 登入失敗",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯藝人",
"HeaderContinueWatching": "繼續觀看",
- "HeaderFavoriteAlbums": "最愛專輯",
+ "HeaderFavoriteAlbums": "最愛的專輯",
"HeaderFavoriteArtists": "最愛的藝人",
"HeaderFavoriteEpisodes": "最愛的劇集",
"HeaderFavoriteShows": "最愛的節目",
@@ -44,10 +44,10 @@
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
"NewVersionIsAvailable": "新版本的 Jellyfin 伺服器可供下載。",
- "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
+ "NotificationOptionApplicationUpdateAvailable": "有可用的更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
- "NotificationOptionAudioPlayback": "開始播放音頻",
- "NotificationOptionAudioPlaybackStopped": "已停止播放音頻",
+ "NotificationOptionAudioPlayback": "開始播放音訊",
+ "NotificationOptionAudioPlaybackStopped": "已停止播放音訊",
"NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已添加新内容",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index b418c7877..166b71b4a 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
@@ -25,7 +26,7 @@ namespace Emby.Server.Implementations.Localization
private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
- private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
+ private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" };
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<LocalizationManager> _logger;
@@ -86,12 +87,10 @@ namespace Emby.Server.Implementations.Localization
var name = parts[0];
dict.Add(name, new ParentalRating(name, value));
}
-#if DEBUG
else
{
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
}
-#endif
}
_allParentalRatings[countryCode] = dict;
@@ -184,80 +183,149 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
- => GetParentalRatingsDictionary().Values;
-
- /// <summary>
- /// Gets the parental ratings dictionary.
- /// </summary>
- /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
- private Dictionary<string, ParentalRating> GetParentalRatingsDictionary()
{
- var countryCode = _configurationManager.Configuration.MetadataCountryCode;
+ // Use server default language for ratings
+ // Fall back to empty list if there are no parental ratings for that language
+ var ratings = GetParentalRatingsDictionary()?.Values.ToList()
+ ?? new List<ParentalRating>();
+
+ // Add common ratings to ensure them being available for selection
+ // Based on the US rating system due to it being the main source of rating in the metadata providers
+ // Unrated
+ if (!ratings.Any(x => x.Value is null))
+ {
+ ratings.Add(new ParentalRating("Unrated", null));
+ }
- if (string.IsNullOrEmpty(countryCode))
+ // Minimum rating possible
+ if (!ratings.Any(x => x.Value == 0))
+ {
+ ratings.Add(new ParentalRating("Approved", 0));
+ }
+
+ // Matches PG (this has different age restrictions depending on country)
+ if (!ratings.Any(x => x.Value == 10))
+ {
+ ratings.Add(new ParentalRating("10", 10));
+ }
+
+ // Matches PG-13
+ if (!ratings.Any(x => x.Value == 13))
+ {
+ ratings.Add(new ParentalRating("13", 13));
+ }
+
+ // Matches TV-14
+ if (!ratings.Any(x => x.Value == 14))
+ {
+ ratings.Add(new ParentalRating("14", 14));
+ }
+
+ // Catchall if max rating of country is less than 21
+ // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned
+ if (!ratings.Any(x => x.Value >= 21))
+ {
+ ratings.Add(new ParentalRating("21", 21));
+ }
+
+ // A lot of countries don't excplicitly have a seperate rating for adult content
+ if (!ratings.Any(x => x.Value == 1000))
+ {
+ ratings.Add(new ParentalRating("XXX", 1000));
+ }
+
+ // A lot of countries don't excplicitly have a seperate rating for banned content
+ if (!ratings.Any(x => x.Value == 1001))
{
- countryCode = "us";
+ ratings.Add(new ParentalRating("Banned", 1001));
}
- return GetRatings(countryCode)
- ?? GetRatings("us")
- ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+ return ratings.OrderBy(r => r.Value);
}
/// <summary>
- /// Gets the ratings.
+ /// Gets the parental ratings dictionary.
/// </summary>
- /// <param name="countryCode">The country code.</param>
- /// <returns>The ratings.</returns>
- private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
+ /// <param name="countryCode">The optional two letter ISO language string.</param>
+ /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
+ private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null)
{
- _allParentalRatings.TryGetValue(countryCode, out var value);
+ // Fallback to server default if no country code is specified.
+ if (string.IsNullOrEmpty(countryCode))
+ {
+ countryCode = _configurationManager.Configuration.MetadataCountryCode;
+ }
- return value;
+ if (_allParentalRatings.TryGetValue(countryCode, out var countryValue))
+ {
+ return countryValue;
+ }
+
+ return null;
}
/// <inheritdoc />
- public int? GetRatingLevel(string rating)
+ public int? GetRatingLevel(string rating, string? countryCode = null)
{
ArgumentException.ThrowIfNullOrEmpty(rating);
+ // Handle unrated content
if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Fairly common for some users to have "Rated R" in their rating field
+ rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
- var ratingsDictionary = GetParentalRatingsDictionary();
-
- if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ // Use rating system matching the language
+ if (!string.IsNullOrEmpty(countryCode))
{
- return value.Value;
+ var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ {
+ return value.Value;
+ }
+ }
+ else
+ {
+ // Fall back to server default language for ratings check
+ // If it has no ratings, use the US ratings
+ var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ {
+ return value.Value;
+ }
}
- // If we don't find anything check all ratings systems
+ // If we don't find anything, check all ratings systems
foreach (var dictionary in _allParentalRatings.Values)
{
- if (dictionary.TryGetValue(rating, out value))
+ if (dictionary.TryGetValue(rating, out var value))
{
return value.Value;
}
}
- // Try splitting by : to handle "Germany: FSK 18"
- var index = rating.IndexOf(':', StringComparison.Ordinal);
- if (index != -1)
+ // Try splitting by : to handle "Germany: FSK-18"
+ if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
{
- var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim();
+ return GetRatingLevel(rating.AsSpan().RightPart(':').ToString());
+ }
- if (!trimmedRating.IsEmpty)
- {
- return GetRatingLevel(trimmedRating.ToString());
- }
+ // Handle prefix country code to handle "DE-18"
+ if (rating.Contains('-', StringComparison.OrdinalIgnoreCase))
+ {
+ var ratingSpan = rating.AsSpan();
+
+ // Extract culture from country prefix
+ var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
+
+ // Check rating system of culture
+ return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
}
- // TODO: Further improve by normalizing out all spaces and dashes
return null;
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv
new file mode 100644
index 000000000..36886ba76
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv
@@ -0,0 +1,11 @@
+E,0
+EC,0
+T,7
+M,18
+AO,18
+UR,18
+RP,18
+X,1000
+XX,1000
+XXX,1000
+XXXX,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
index 11f4ed94c..4ab808ae9 100644
--- a/Emby.Server.Implementations/Localization/Ratings/au.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/au.csv
@@ -1,7 +1,13 @@
-AU-G,1
-AU-PG,5
-AU-M,6
-AU-MA15+,7
-AU-R18+,9
-AU-X18+,10
-AU-RC,11
+Exempt,0
+G,0
+7+,7
+M,15
+MA,15
+MA15+,15
+PG,16
+16+,16
+R,18
+R18+,18
+X18+,18
+18+,18
+X,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv
index d3937caf7..d171a7132 100644
--- a/Emby.Server.Implementations/Localization/Ratings/be.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/be.csv
@@ -1,6 +1,11 @@
-BE-AL,1
-BE-MG6,2
-BE-6,3
-BE-9,5
-BE-12,6
-BE-16,8
+AL,0
+KT,0
+TOUS,0
+MG6,6
+6,6
+9,9
+KNT,12
+12,12
+14,14
+16,16
+18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv
index e5edaf62c..5ec1eb262 100644
--- a/Emby.Server.Implementations/Localization/Ratings/br.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/br.csv
@@ -1,6 +1,8 @@
-BR-L,1
-BR-10,5
-BR-12,7
-BR-14,8
-BR-16,8
-BR-18,9
+Livre,0
+L,0
+ER,9
+10,10
+12,12
+14,14
+16,16
+18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv
index 5aef0580f..336ee2806 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ca.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ca.csv
@@ -1,6 +1,20 @@
-CA-G,1
-CA-PG,5
-CA-14A,7
-CA-A,8
-CA-18A,9
-CA-R,10
+E,0
+G,0
+TV-Y,0
+TV-G,0
+TV-Y7,7
+TV-Y7-FV,7
+PG,9
+TV-PG,9
+PG-13,13
+13+,13
+TV-14,14
+14A,14
+16+,16
+NC-17,17
+R,18
+TV-MA,18
+18A,18
+18+,18
+A,1000
+Prohibited,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv
index 9684fa052..e1e96c590 100644
--- a/Emby.Server.Implementations/Localization/Ratings/co.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/co.csv
@@ -1,8 +1,7 @@
-CO-T,1
-CO-7,5
-CO-12,7
-CO-15,8
-CO-18,10
-CO-X,100
-CO-BANNED,15
-CO-E,15
+T,0
+7,7
+12,12
+15,15
+18,18
+X,1000
+Prohibited,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv
index f944a140d..d633a5dab 100644
--- a/Emby.Server.Implementations/Localization/Ratings/de.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/de.csv
@@ -1,10 +1,12 @@
-DE-0,1
-FSK-0,1
-DE-6,5
-FSK-6,5
-DE-12,7
-FSK-12,7
-DE-16,8
-FSK-16,8
-DE-18,9
-FSK-18,9
+Educational,0
+Infoprogramm,0
+FSK-0,0
+0,0
+FSK-6,6
+6,6
+FSK-12,12
+12,12
+FSK-16,16
+16,16
+FSK-18,18
+18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv
index 5364ae1f2..4ef63b2ea 100644
--- a/Emby.Server.Implementations/Localization/Ratings/dk.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/dk.csv
@@ -1,4 +1,7 @@
-DA-A,1
-DA-7,5
-DA-11,6
-DA-15,8
+F,0
+A,0
+7,7
+11,11
+12,12
+15,15
+16,16
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv
index 887d91ba6..0bc1d3f7d 100644
--- a/Emby.Server.Implementations/Localization/Ratings/es.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/es.csv
@@ -1,6 +1,24 @@
-ES-A,1
-ES-APTA,1
-ES-7,3
-ES-12,6
-ES-16,8
-ES-18,11
+A,0
+A/fig,0
+A/i,0
+A/fig/i,0
+APTA,0
+TP,0
+0+,0
+6+,6
+7/fig,7
+7/i,7
+7/i/fig,7
+7,7
+9+,9
+10,10
+12,12
+12/fig,12
+13,13
+14,14
+16,16
+16/fig,16
+18,18
+18/fig,18
+X,1000
+Banned,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv
index 782785890..7ff92f259 100644
--- a/Emby.Server.Implementations/Localization/Ratings/fi.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/fi.csv
@@ -1,10 +1,10 @@
-FI-S,1
-FI-T,1
-FI-7,4
-FI-12,5
-FI-16,8
-FI-18,9
-FI-K7,4
-FI-K12,5
-FI-K16,8
-FI-K18,9
+S,0
+T,0
+K7,7
+7,7
+K12,12
+12,12
+K16,16
+16,16
+K18,18
+18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv
index f586a3fa9..774a70589 100644
--- a/Emby.Server.Implementations/Localization/Ratings/fr.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/fr.csv
@@ -1,5 +1,12 @@
-FR-U,1
-FR-10,5
-FR-12,7
-FR-16,9
-FR-18,10
+Public Averti,0
+Tous Publics,0
+U,0
+0+,0
+6+,6
+9+,9
+10,10
+12,12
+14+,14
+16,16
+18,18
+X,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv
index c1f7d0452..75b1c2058 100644
--- a/Emby.Server.Implementations/Localization/Ratings/gb.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/gb.csv
@@ -1,7 +1,22 @@
-GB-U,1
-GB-PG,5
-GB-12,6
-GB-12A,7
-GB-15,8
-GB-18,9
-GB-R18,15
+All,0
+E,0
+G,0
+U,0
+0+,0
+6+,6
+7+,7
+PG,8
+9+,9
+12,12
+12+,12
+12A,12
+Teen,13
+13+,13
+14+,14
+15,15
+16,16
+Caution,18
+18,18
+Mature,1000
+Adult,1000
+R18,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv
index e42be5cd4..6ef2e5012 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ie.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ie.csv
@@ -1,6 +1,9 @@
-IE-G,1
-IE-PG,5
-IE-12A,7
-IE-15A,8
-IE-16,9
-IE-18,10
+G,4
+PG,12
+12,12
+12A,12
+12PG,12
+15,15
+15A,15
+16,16
+18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv
index a8fc2d143..bfb5fdaae 100644
--- a/Emby.Server.Implementations/Localization/Ratings/jp.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/jp.csv
@@ -1,4 +1,11 @@
-JP-G,1
-JP-PG12,7
-JP-15+,8
-JP-18+,10
+A,0
+G,0
+B,12
+PG12,12
+C,15
+15+,15
+R15+,15
+16+,16
+D,17
+Z,18
+18+,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv
index d546bff53..e26b32b67 100644
--- a/Emby.Server.Implementations/Localization/Ratings/kz.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/kz.csv
@@ -1,7 +1,6 @@
-KZ-6-,0
-KZ-6+,6
-KZ-12+,12
-KZ-14+,14
-KZ-16+,16
-KZ-18+,18
-KZ-21+,21
+K,0
+БА,12
+Б14,14
+E16,16
+E18,18
+HA,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv
index 785a8ba22..305912f23 100644
--- a/Emby.Server.Implementations/Localization/Ratings/mx.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/mx.csv
@@ -1,6 +1,6 @@
-MX-AA,1
-MX-A,5
-MX-B,7
-MX-B-15,8
-MX-C,9
-MX-D,10
+A,0
+AA,0
+B,12
+B-15,15
+C,18
+D,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv
index 8c005092e..44f372b2d 100644
--- a/Emby.Server.Implementations/Localization/Ratings/nl.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/nl.csv
@@ -1,6 +1,8 @@
-NL-AL,1
-NL-MG6,2
-NL-6,3
-NL-9,5
-NL-12,6
-NL-16,8
+AL,0
+MG6,6
+6,6
+9,9
+12,12
+14,14
+16,16
+18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv
index 127407be8..c8f8e93db 100644
--- a/Emby.Server.Implementations/Localization/Ratings/no.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/no.csv
@@ -1,6 +1,9 @@
-NO-A,1
-NO-6,3
-NO-9,4
-NO-12,5
-NO-15,8
-NO-18,9
+A,0
+6,6
+7,7
+9,9
+11,11
+12,12
+15,15
+18,18
+Not approved,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv
index bba99b764..f617f0c39 100644
--- a/Emby.Server.Implementations/Localization/Ratings/nz.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/nz.csv
@@ -1,11 +1,15 @@
-NZ-G,1
-NZ-PG,5
-NZ-M,6
-NZ-R13,7
-NZ-RP13,7
-NZ-R15,8
-NZ-RP16,9
-NZ-R16,9
-NZ-R18,10
-NZ-R,10
-NZ-MA,10
+Exempt,0
+G,0
+GY,13
+PG,13
+R13,13
+RP13,13
+R15,15
+M,16
+R16,16
+RP16,16
+GA,18
+R18,18
+MA,1000
+R,1001
+Objectionable,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv
index 4089b282f..44c23e248 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ro.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ro.csv
@@ -1 +1,6 @@
-RO-AG,1
+AG,0
+AP-12,12
+N-15,15
+IM-18,18
+IM-18-XXX,1000
+IC,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv
index 1bc94affd..8b264070b 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ru.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ru.csv
@@ -1,5 +1,6 @@
-RU-0+,1
-RU-6+,3
-RU-12+,7
-RU-16+,9
-RU-18+,10
+0+,0
+6+,6
+12+,12
+16+,16
+18+,18
+Refused classification,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv
index 1443c07df..e129c3561 100644
--- a/Emby.Server.Implementations/Localization/Ratings/se.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/se.csv
@@ -1,5 +1,10 @@
-SE-Btl,1
-SE-Barntillåten,1
-SE-7,3
-SE-11,5
-SE-15,8
+Alla,0
+Barntillåten,0
+Btl,0
+0+,0
+7,7
+9+,9
+10+,10
+11,11
+14,14
+15,15
diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv
index 6c8005b3f..75b1c2058 100644
--- a/Emby.Server.Implementations/Localization/Ratings/uk.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/uk.csv
@@ -1,7 +1,22 @@
-UK-U,1
-UK-PG,5
-UK-12,7
-UK-12A,7
-UK-15,9
-UK-18,10
-UK-R18,15
+All,0
+E,0
+G,0
+U,0
+0+,0
+6+,6
+7+,7
+PG,8
+9+,9
+12,12
+12+,12
+12A,12
+Teen,13
+13+,13
+14+,14
+15,15
+16,16
+Caution,18
+18,18
+Mature,1000
+Adult,1000
+R18,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv
index 34c897fe3..d103ddf42 100644
--- a/Emby.Server.Implementations/Localization/Ratings/us.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/us.csv
@@ -1,23 +1,50 @@
-TV-Y,1
-APPROVED,1
-G,1
-E,1
-EC,1
-TV-G,1
-TV-Y7,3
-TV-Y7-FV,4
-PG,5
-TV-PG,5
-PG-13,7
-T,7
-TV-14,8
-R,9
-M,9
-TV-MA,9
-NC-17,10
-AO,15
-RP,15
-UR,15
-NR,15
-X,15
-XXX,100
+Approved,0
+G,0
+TV-G,0
+TV-Y,0
+TV-Y7,7
+TV-Y7-FV,7
+PG,10
+PG-13,13
+TV-PG,13
+TV-PG-D,13
+TV-PG-L,13
+TV-PG-S,13
+TV-PG-V,13
+TV-PG-DL,13
+TV-PG-DS,13
+TV-PG-DV,13
+TV-PG-LS,13
+TV-PG-LV,13
+TV-PG-SV,13
+TV-PG-DLS,13
+TV-PG-DLV,13
+TV-PG-DSV,13
+TV-PG-LSV,13
+TV-PG-DLSV,13
+TV-14,14
+TV-14-D,14
+TV-14-L,14
+TV-14-S,14
+TV-14-V,14
+TV-14-DL,14
+TV-14-DS,14
+TV-14-DV,14
+TV-14-LS,14
+TV-14-LV,14
+TV-14-SV,14
+TV-14-DLS,14
+TV-14-DLV,14
+TV-14-DSV,14
+TV-14-LSV,14
+TV-14-DLSV,14
+NC-17,17
+R,17
+TV-MA,17
+TV-MA-L,17
+TV-MA-S,17
+TV-MA-V,17
+TV-MA-LS,17
+TV-MA-LV,17
+TV-MA-SV,17
+TV-MA-LSV,17
diff --git a/Emby.Server.Implementations/Plugins/PluginLoadContext.cs b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs
new file mode 100644
index 000000000..d04e9cf68
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Emby.Server.Implementations.Plugins;
+
+/// <summary>
+/// A custom <see cref="AssemblyLoadContext"/> for loading Jellyfin plugins.
+/// </summary>
+public class PluginLoadContext : AssemblyLoadContext
+{
+ private readonly AssemblyDependencyResolver _resolver;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginLoadContext"/> class.
+ /// </summary>
+ /// <param name="path">The path of the plugin assembly.</param>
+ public PluginLoadContext(string path) : base(true)
+ {
+ _resolver = new AssemblyDependencyResolver(path);
+ }
+
+ /// <inheritdoc />
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
+ if (assemblyPath is not null)
+ {
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+
+ return null;
+ }
+}
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 14e7c2269..7c23254a1 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
+using System.Runtime.Loader;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
@@ -30,6 +31,7 @@ namespace Emby.Server.Implementations.Plugins
{
private readonly string _pluginsPath;
private readonly Version _appVersion;
+ private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost;
@@ -76,6 +78,8 @@ namespace Emby.Server.Implementations.Plugins
_appHost = appHost;
_minimumVersion = new Version(0, 0, 0, 1);
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
+
+ _assemblyLoadContexts = new List<AssemblyLoadContext>();
}
private IHttpClientFactory HttpClientFactory
@@ -119,43 +123,78 @@ namespace Emby.Server.Implementations.Plugins
continue;
}
+ var assemblyLoadContext = new PluginLoadContext(plugin.Path);
+ _assemblyLoadContexts.Add(assemblyLoadContext);
+
+ var assemblies = new List<Assembly>(plugin.DllFiles.Count);
+ var loadedAll = true;
+
foreach (var file in plugin.DllFiles)
{
- Assembly assembly;
try
{
- assembly = Assembly.LoadFrom(file);
-
- // Load all required types to verify that the plugin will load
- assembly.GetTypes();
+ assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file));
}
catch (FileLoadException ex)
{
- _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
+ _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
- continue;
+ loadedAll = false;
+ break;
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", file);
+ ChangePluginState(plugin, PluginStatus.Malfunctioned);
+ loadedAll = false;
+ break;
+ }
+ }
+
+ if (!loadedAll)
+ {
+ continue;
+ }
+
+ foreach (var assembly in assemblies)
+ {
+ try
+ {
+ // Load all required types to verify that the plugin will load
+ assembly.GetTypes();
}
catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception
{
- _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
+ _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin", assembly.Location);
ChangePluginState(plugin, PluginStatus.NotSupported);
- continue;
+ break;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
- _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
+ _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", assembly.Location);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
- continue;
+ break;
}
- _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
+ _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Location);
yield return assembly;
}
}
}
+ /// <inheritdoc />
+ public void UnloadAssemblies()
+ {
+ foreach (var assemblyLoadContext in _assemblyLoadContexts)
+ {
+ assemblyLoadContext.Unload();
+ }
+ }
+
/// <summary>
/// Creates all the plugin instances.
/// </summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index ee9aa8569..1af2c96d2 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -93,11 +93,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger)
{
ArgumentNullException.ThrowIfNull(scheduledTask);
-
ArgumentNullException.ThrowIfNull(applicationPaths);
-
ArgumentNullException.ThrowIfNull(taskManager);
-
ArgumentNullException.ThrowIfNull(logger);
ScheduledTask = scheduledTask;
@@ -332,7 +329,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return;
}
- _logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name);
+ _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name);
trigger.Stop();
@@ -378,7 +375,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
CurrentCancellationTokenSource = new CancellationTokenSource();
- _logger.LogInformation("Executing {0}", Name);
+ _logger.LogDebug("Executing {0}", Name);
((TaskManager)_taskManager).OnTaskExecuting(this);
@@ -406,7 +403,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error");
+ _logger.LogError(ex, "Error executing Scheduled Task");
failureException = ex;
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 63f0beb10..42c30c959 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -43,9 +41,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
- public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+ public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
- public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+ public event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
/// <summary>
/// Gets the list of Scheduled Tasks.
@@ -134,7 +132,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
var type = scheduledTask.ScheduledTask.GetType();
- _logger.LogInformation("Queuing task {0}", type.Name);
+ _logger.LogDebug("Queuing task {0}", type.Name);
lock (_taskQueue)
{
@@ -174,7 +172,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
var type = task.ScheduledTask.GetType();
- _logger.LogInformation("Queuing task {0}", type.Name);
+ _logger.LogDebug("Queuing task {0}", type.Name);
lock (_taskQueue)
{
@@ -256,9 +254,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void ExecuteQueuedTasks()
{
- _logger.LogInformation("ExecuteQueuedTasks");
-
- // Execute queued tasks
lock (_taskQueue)
{
var list = new List<Tuple<Type, TaskOptions>>();
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 1efacd856..1f3cb9b63 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
private readonly ILogger<OptimizeDatabaseTask> _logger;
private readonly ILocalizationManager _localization;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <summary>
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public OptimizeDatabaseTask(
ILogger<OptimizeDatabaseTask> logger,
ILocalizationManager localization,
- IDbContextFactory<JellyfinDb> provider)
+ IDbContextFactory<JellyfinDbContext> provider)
{
_logger = logger;
_localization = localization;
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index aebb55907..4e427b1a4 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -58,7 +56,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// The KeepAlive cancellation token.
/// </summary>
- private CancellationTokenSource _keepAliveCancellationToken;
+ private CancellationTokenSource? _keepAliveCancellationToken;
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
@@ -105,7 +103,7 @@ namespace Emby.Server.Implementations.Session
}
}
- private async Task<SessionInfo> GetSession(HttpContext httpContext, string remoteEndpoint)
+ private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint)
{
if (!httpContext.User.Identity?.IsAuthenticated ?? false)
{
@@ -138,8 +136,13 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="sender">The WebSocket.</param>
/// <param name="e">The event arguments.</param>
- private void OnWebSocketClosed(object sender, EventArgs e)
+ private void OnWebSocketClosed(object? sender, EventArgs e)
{
+ if (sender is null)
+ {
+ return;
+ }
+
var webSocket = (IWebSocketConnection)sender;
_logger.LogDebug("WebSocket {0} is closed.", webSocket);
RemoveWebSocket(webSocket);
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 646bafbb5..753e58324 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
ArgumentNullException.ThrowIfNull(x);
-
ArgumentNullException.ThrowIfNull(y);
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index 0bd9600b9..5b6c64f63 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
}
- private static string GetValue(BaseItem item)
+ private static string? GetValue(BaseItem? item)
{
var hasSeries = item as IHasSeries;
-
return hasSeries?.FindSeriesSortName();
}
}
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index 628b9b3dd..19abafe19 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
ArgumentNullException.ThrowIfNull(x);
-
ArgumentNullException.ThrowIfNull(y);
return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index c3df7c47e..2759d20de 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -24,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetDate(x).CompareTo(GetDate(y));
}
@@ -34,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>DateTime.</returns>
- private static DateTime GetDate(BaseItem x)
+ private static DateTime GetDate(BaseItem? x)
{
if (x is LiveTvProgram hasStartDate)
{
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 457c06271..89d10f3d2 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
ArgumentNullException.ThrowIfNull(x);
-
ArgumentNullException.ThrowIfNull(y);
return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 967f90b55..f0e173f0b 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -42,7 +40,7 @@ namespace Emby.Server.Implementations.TV
throw new ArgumentException("User not found");
}
- string presentationUniqueKey = null;
+ string? presentationUniqueKey = null;
if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
{
if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
@@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.TV
throw new ArgumentException("User not found");
}
- string presentationUniqueKey = null;
+ string? presentationUniqueKey = null;
int? limit = null;
if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
{
@@ -168,7 +166,7 @@ namespace Emby.Server.Implementations.TV
return !anyFound && i.LastWatchedDate == DateTime.MinValue;
})
.Select(i => i.GetEpisodeFunction())
- .Where(i => i is not null);
+ .Where(i => i is not null)!;
}
private static string GetUniqueSeriesKey(Episode episode)
@@ -185,7 +183,7 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up.
/// </summary>
/// <returns>Task{Episode}.</returns>
- private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
+ private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
{
var lastQuery = new InternalItemsQuery(user)
{
@@ -209,7 +207,7 @@ namespace Emby.Server.Implementations.TV
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
- Episode GetEpisode()
+ Episode? GetEpisode()
{
var nextQuery = new InternalItemsQuery(user)
{
diff --git a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
index fbe68b6b9..a6c89bab8 100644
--- a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
@@ -2,29 +2,28 @@
using System;
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Internal produces image attribute.
+/// </summary>
+[AttributeUsage(AttributeTargets.Method)]
+public class AcceptsFileAttribute : Attribute
{
+ private readonly string[] _contentTypes;
+
/// <summary>
- /// Internal produces image attribute.
+ /// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
/// </summary>
- [AttributeUsage(AttributeTargets.Method)]
- public class AcceptsFileAttribute : Attribute
+ /// <param name="contentTypes">Content types this endpoint produces.</param>
+ public AcceptsFileAttribute(params string[] contentTypes)
{
- private readonly string[] _contentTypes;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
- /// </summary>
- /// <param name="contentTypes">Content types this endpoint produces.</param>
- public AcceptsFileAttribute(params string[] contentTypes)
- {
- _contentTypes = contentTypes;
- }
-
- /// <summary>
- /// Gets the configured content types.
- /// </summary>
- /// <returns>the configured content types.</returns>
- public string[] ContentTypes => _contentTypes;
+ _contentTypes = contentTypes;
}
+
+ /// <summary>
+ /// Gets the configured content types.
+ /// </summary>
+ /// <returns>the configured content types.</returns>
+ public string[] ContentTypes => _contentTypes;
}
diff --git a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
index 244a29da4..57433202e 100644
--- a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Produces file attribute of "image/*".
+/// </summary>
+public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
{
+ private const string ContentType = "image/*";
+
/// <summary>
- /// Produces file attribute of "image/*".
+ /// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
/// </summary>
- public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
+ public AcceptsImageFileAttribute()
+ : base(ContentType)
{
- private const string ContentType = "image/*";
-
- /// <summary>
- /// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
- /// </summary>
- public AcceptsImageFileAttribute()
- : base(ContentType)
- {
- }
}
}
diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
index 4dcf5976a..cbd32ed82 100644
--- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
+++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
@@ -2,29 +2,28 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Routing;
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Identifies an action that supports the HTTP GET method.
+/// </summary>
+public sealed class HttpSubscribeAttribute : HttpMethodAttribute
{
+ private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
+
/// <summary>
- /// Identifies an action that supports the HTTP GET method.
+ /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
/// </summary>
- public sealed class HttpSubscribeAttribute : HttpMethodAttribute
+ public HttpSubscribeAttribute()
+ : base(_supportedMethods)
{
- private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
- /// </summary>
- public HttpSubscribeAttribute()
- : base(_supportedMethods)
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
- /// </summary>
- /// <param name="template">The route template. May not be null.</param>
- public HttpSubscribeAttribute(string template)
- : base(_supportedMethods, template)
- => ArgumentNullException.ThrowIfNull(template);
}
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
+ /// </summary>
+ /// <param name="template">The route template. May not be null.</param>
+ public HttpSubscribeAttribute(string template)
+ : base(_supportedMethods, template)
+ => ArgumentNullException.ThrowIfNull(template);
}
diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
index d0238424a..f4a6dcdaf 100644
--- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
+++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
@@ -2,29 +2,28 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Routing;
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Identifies an action that supports the HTTP GET method.
+/// </summary>
+public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute
{
+ private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
+
/// <summary>
- /// Identifies an action that supports the HTTP GET method.
+ /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
/// </summary>
- public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute
+ public HttpUnsubscribeAttribute()
+ : base(_supportedMethods)
{
- private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
- /// </summary>
- public HttpUnsubscribeAttribute()
- : base(_supportedMethods)
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
- /// </summary>
- /// <param name="template">The route template. May not be null.</param>
- public HttpUnsubscribeAttribute(string template)
- : base(_supportedMethods, template)
- => ArgumentNullException.ThrowIfNull(template);
}
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
+ /// </summary>
+ /// <param name="template">The route template. May not be null.</param>
+ public HttpUnsubscribeAttribute(string template)
+ : base(_supportedMethods, template)
+ => ArgumentNullException.ThrowIfNull(template);
}
diff --git a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
index 514e7ce97..bf64fef5d 100644
--- a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
+++ b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
@@ -1,12 +1,11 @@
using System;
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Attribute to mark a parameter as obsolete.
+/// </summary>
+[AttributeUsage(AttributeTargets.Parameter)]
+public sealed class ParameterObsoleteAttribute : Attribute
{
- /// <summary>
- /// Attribute to mark a parameter as obsolete.
- /// </summary>
- [AttributeUsage(AttributeTargets.Parameter)]
- public sealed class ParameterObsoleteAttribute : Attribute
- {
- }
}
diff --git a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
index 9fc25f192..7ce09c299 100644
--- a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Produces file attribute of "image/*".
+/// </summary>
+public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
{
+ private const string ContentType = "audio/*";
+
/// <summary>
- /// Produces file attribute of "image/*".
+ /// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class.
/// </summary>
- public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
+ public ProducesAudioFileAttribute()
+ : base(ContentType)
{
- private const string ContentType = "audio/*";
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class.
- /// </summary>
- public ProducesAudioFileAttribute()
- : base(ContentType)
- {
- }
}
}
diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
index d8e4141ac..c728f68e0 100644
--- a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
@@ -2,29 +2,28 @@
using System;
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Internal produces image attribute.
+/// </summary>
+[AttributeUsage(AttributeTargets.Method)]
+public class ProducesFileAttribute : Attribute
{
+ private readonly string[] _contentTypes;
+
/// <summary>
- /// Internal produces image attribute.
+ /// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class.
/// </summary>
- [AttributeUsage(AttributeTargets.Method)]
- public class ProducesFileAttribute : Attribute
+ /// <param name="contentTypes">Content types this endpoint produces.</param>
+ public ProducesFileAttribute(params string[] contentTypes)
{
- private readonly string[] _contentTypes;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class.
- /// </summary>
- /// <param name="contentTypes">Content types this endpoint produces.</param>
- public ProducesFileAttribute(params string[] contentTypes)
- {
- _contentTypes = contentTypes;
- }
-
- /// <summary>
- /// Gets the configured content types.
- /// </summary>
- /// <returns>the configured content types.</returns>
- public string[] ContentTypes => _contentTypes;
+ _contentTypes = contentTypes;
}
+
+ /// <summary>
+ /// Gets the configured content types.
+ /// </summary>
+ /// <returns>the configured content types.</returns>
+ public string[] ContentTypes => _contentTypes;
}
diff --git a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
index 1e5b542e2..f145a061e 100644
--- a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Produces file attribute of "image/*".
+/// </summary>
+public sealed class ProducesImageFileAttribute : ProducesFileAttribute
{
+ private const string ContentType = "image/*";
+
/// <summary>
- /// Produces file attribute of "image/*".
+ /// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class.
/// </summary>
- public sealed class ProducesImageFileAttribute : ProducesFileAttribute
+ public ProducesImageFileAttribute()
+ : base(ContentType)
{
- private const string ContentType = "image/*";
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class.
- /// </summary>
- public ProducesImageFileAttribute()
- : base(ContentType)
- {
- }
}
}
diff --git a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
index 5b15cb1a5..c03ed740c 100644
--- a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Produces file attribute of "image/*".
+/// </summary>
+public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
{
+ private const string ContentType = "application/x-mpegURL";
+
/// <summary>
- /// Produces file attribute of "image/*".
+ /// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class.
/// </summary>
- public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
+ public ProducesPlaylistFileAttribute()
+ : base(ContentType)
{
- private const string ContentType = "application/x-mpegURL";
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class.
- /// </summary>
- public ProducesPlaylistFileAttribute()
- : base(ContentType)
- {
- }
}
}
diff --git a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
index 6857d45ec..10dec0c00 100644
--- a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Attributes
+namespace Jellyfin.Api.Attributes;
+
+/// <summary>
+/// Produces file attribute of "video/*".
+/// </summary>
+public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
{
+ private const string ContentType = "video/*";
+
/// <summary>
- /// Produces file attribute of "video/*".
+ /// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class.
/// </summary>
- public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
+ public ProducesVideoFileAttribute()
+ : base(ContentType)
{
- private const string ContentType = "video/*";
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class.
- /// </summary>
- public ProducesVideoFileAttribute()
- : base(ContentType)
- {
- }
}
}
diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
index d4b1ffb06..741b88ea9 100644
--- a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
+++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -29,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement)
{
- var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress;
+ var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
// Loopback will be on LAN, so we can accept null.
if (ip is null || _networkManager.IsInLocalNetwork(ip))
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
deleted file mode 100644
index 8e5e66d64..000000000
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System.Security.Claims;
-using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth
-{
- /// <summary>
- /// Base authorization handler.
- /// </summary>
- /// <typeparam name="T">Type of Authorization Requirement.</typeparam>
- public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
- where T : IAuthorizationRequirement
- {
- private readonly IUserManager _userManager;
- private readonly INetworkManager _networkManager;
- private readonly IHttpContextAccessor _httpContextAccessor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- protected BaseAuthorizationHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- {
- _userManager = userManager;
- _networkManager = networkManager;
- _httpContextAccessor = httpContextAccessor;
- }
-
- /// <summary>
- /// Validate authenticated claims.
- /// </summary>
- /// <param name="claimsPrincipal">Request claims.</param>
- /// <param name="ignoreSchedule">Whether to ignore parental control.</param>
- /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
- /// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
- /// <returns>Validated claim status.</returns>
- protected bool ValidateClaims(
- ClaimsPrincipal claimsPrincipal,
- bool ignoreSchedule = false,
- bool localAccessOnly = false,
- bool requiredDownloadPermission = false)
- {
- // ApiKey is currently global admin, always allow.
- var isApiKey = claimsPrincipal.GetIsApiKey();
- if (isApiKey)
- {
- return true;
- }
-
- // Ensure claim has userId.
- var userId = claimsPrincipal.GetUserId();
- if (userId.Equals(default))
- {
- return false;
- }
-
- // Ensure userId links to a valid user.
- var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return false;
- }
-
- // Ensure user is not disabled.
- if (user.HasPermission(PermissionKind.IsDisabled))
- {
- return false;
- }
-
- var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
- && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
-
- // User cannot access remotely and user is remote
- if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
- {
- return false;
- }
-
- if (localAccessOnly && !isInLocalNetwork)
- {
- return false;
- }
-
- // User attempting to access out of parental control hours.
- if (!ignoreSchedule
- && !user.HasPermission(PermissionKind.IsAdministrator)
- && !user.IsParentalScheduleAllowed())
- {
- return false;
- }
-
- // User attempting to download without permission.
- if (requiredDownloadPermission
- && !user.HasPermission(PermissionKind.EnableContentDownloading))
- {
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
index be77b7a4e..de271ab64 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -1,4 +1,8 @@
using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -9,8 +13,12 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
/// <summary>
/// Default authorization handler.
/// </summary>
- public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
+ public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{
+ private readonly IUserManager _userManager;
+ private readonly INetworkManager _networkManager;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
/// </summary>
@@ -21,21 +29,63 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
{
+ _userManager = userManager;
+ _networkManager = networkManager;
+ _httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{
- var validated = ValidateClaims(context.User);
- if (validated)
+ var isApiKey = context.User.GetIsApiKey();
+ var userId = context.User.GetUserId();
+ // This likely only happens during the wizard, so skip the default checks and let any other handlers do it
+ if (!isApiKey && userId.Equals(default))
+ {
+ return Task.CompletedTask;
+ }
+
+ if (isApiKey)
+ {
+ // Api keys are unrestricted.
+ context.Succeed(requirement);
+ return Task.CompletedTask;
+ }
+
+ var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
+ && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ // User cannot access remotely and user is remote
+ if (!isInLocalNetwork && !user.HasPermission(PermissionKind.EnableRemoteAccess))
+ {
+ context.Fail();
+ return Task.CompletedTask;
+ }
+
+ // Admins can do everything
+ if (context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
+ return Task.CompletedTask;
}
- else
+
+ // It's not great to have this check, but parental schedule must usually be honored except in a few rare cases
+ if (requirement.ValidateParentalSchedule && !user.IsParentalScheduleAllowed())
{
context.Fail();
+ return Task.CompletedTask;
+ }
+
+ // Only succeed if the requirement isn't a subclass as any subclassed requirement will handle success in its own handler
+ if (requirement.GetType() == typeof(DefaultAuthorizationRequirement))
+ {
+ context.Succeed(requirement);
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
index 7cea00b69..5ba1bc330 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
@@ -7,5 +7,18 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
/// </summary>
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultAuthorizationRequirement"/> class.
+ /// </summary>
+ /// <param name="validateParentalSchedule">A value indicating whether to validate parental schedule.</param>
+ public DefaultAuthorizationRequirement(bool validateParentalSchedule = true)
+ {
+ ValidateParentalSchedule = validateParentalSchedule;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether to ignore parental schedule.
+ /// </summary>
+ public bool ValidateParentalSchedule { get; }
}
}
diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
deleted file mode 100644
index b61680ab1..000000000
--- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.DownloadPolicy
-{
- /// <summary>
- /// Download authorization handler.
- /// </summary>
- public class DownloadHandler : BaseAuthorizationHandler<DownloadRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="DownloadHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public DownloadHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
- {
- var validated = ValidateClaims(context.User);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
deleted file mode 100644
index b0a72a9de..000000000
--- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.DownloadPolicy
-{
- /// <summary>
- /// The download permission requirement.
- /// </summary>
- public class DownloadRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
deleted file mode 100644
index 31482a930..000000000
--- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
-{
- /// <summary>
- /// Ignore parental control schedule and allow before startup wizard has been completed.
- /// </summary>
- public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<FirstTimeOrIgnoreParentalControlSetupRequirement>
- {
- private readonly IConfigurationManager _configurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FirstTimeOrIgnoreParentalControlSetupHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- public FirstTimeOrIgnoreParentalControlSetupHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor,
- IConfigurationManager configurationManager)
- : base(userManager, networkManager, httpContextAccessor)
- {
- _configurationManager = configurationManager;
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement)
- {
- if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
- {
- context.Succeed(requirement);
- return Task.CompletedTask;
- }
-
- var validated = ValidateClaims(context.User, ignoreSchedule: true);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
deleted file mode 100644
index 00aaec334..000000000
--- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
-{
- /// <summary>
- /// First time setup or ignore parental controls requirement.
- /// </summary>
- public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
deleted file mode 100644
index dd0bd4ec2..000000000
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
-{
- /// <summary>
- /// Authorization handler for requiring first time setup or default privileges.
- /// </summary>
- public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
- {
- private readonly IConfigurationManager _configurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
- /// </summary>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public FirstTimeSetupOrDefaultHandler(
- IConfigurationManager configurationManager,
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- _configurationManager = configurationManager;
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement)
- {
- if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
- {
- context.Succeed(requirement);
- return Task.CompletedTask;
- }
-
- var validated = ValidateClaims(context.User);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
deleted file mode 100644
index f7366bd7a..000000000
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
-{
- /// <summary>
- /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
- /// </summary>
- public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs
deleted file mode 100644
index 51ba637b6..000000000
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
-{
- /// <summary>
- /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
- /// </summary>
- public class FirstTimeSetupOrElevatedRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
index 90b76ee99..28ba25850 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
@@ -1,39 +1,36 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
+namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
{
/// <summary>
- /// Authorization handler for requiring first time setup or elevated privileges.
+ /// Authorization handler for requiring first time setup or default privileges.
/// </summary>
- public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
+ public class FirstTimeSetupHandler : AuthorizationHandler<FirstTimeSetupRequirement>
{
private readonly IConfigurationManager _configurationManager;
+ private readonly IUserManager _userManager;
/// <summary>
- /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
+ /// Initializes a new instance of the <see cref="FirstTimeSetupHandler" /> class.
/// </summary>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public FirstTimeSetupOrElevatedHandler(
+ public FirstTimeSetupHandler(
IConfigurationManager configurationManager,
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
+ IUserManager userManager)
{
_configurationManager = configurationManager;
+ _userManager = userManager;
}
/// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
@@ -41,14 +38,27 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
return Task.CompletedTask;
}
- var validated = ValidateClaims(context.User);
- if (validated && context.User.IsInRole(UserRoles.Administrator))
+ if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
+ {
+ context.Fail();
+ return Task.CompletedTask;
+ }
+
+ if (!requirement.ValidateParentalSchedule)
{
context.Succeed(requirement);
+ return Task.CompletedTask;
}
- else
+
+ var user = _userManager.GetUserById(context.User.GetUserId());
+ if (user is null)
{
- context.Fail();
+ throw new ResourceNotFoundException();
+ }
+
+ if (user.IsParentalScheduleAllowed())
+ {
+ context.Succeed(requirement);
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs
new file mode 100644
index 000000000..6252a2feb
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs
@@ -0,0 +1,25 @@
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
+{
+ /// <summary>
+ /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
+ /// </summary>
+ public class FirstTimeSetupRequirement : DefaultAuthorizationRequirement
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirstTimeSetupRequirement"/> class.
+ /// </summary>
+ /// <param name="validateParentalSchedule">A value indicating whether to ignore parental schedule.</param>
+ /// <param name="requireAdmin">A value indicating whether administrator role is required.</param>
+ public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) : base(validateParentalSchedule)
+ {
+ RequireAdmin = requireAdmin;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether administrator role is required.
+ /// </summary>
+ public bool RequireAdmin { get; }
+ }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
deleted file mode 100644
index a7623556a..000000000
--- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
-{
- /// <summary>
- /// Escape schedule controls handler.
- /// </summary>
- public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public IgnoreParentalControlHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
- {
- var validated = ValidateClaims(context.User, ignoreSchedule: true);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
deleted file mode 100644
index cdad74270..000000000
--- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
-{
- /// <summary>
- /// Escape schedule controls requirement.
- /// </summary>
- public class IgnoreParentalControlRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
index 14722aa57..6ed6fc90b 100644
--- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
@@ -1,7 +1,7 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -10,27 +10,38 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
/// <summary>
/// Local access or require elevated privileges handler.
/// </summary>
- public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
+ public class LocalAccessOrRequiresElevationHandler : AuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
{
+ private readonly INetworkManager _networkManager;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
/// <summary>
/// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class.
/// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public LocalAccessOrRequiresElevationHandler(
- IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
{
+ _networkManager = networkManager;
+ _httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
{
- var validated = ValidateClaims(context.User, localAccessOnly: true);
- if (validated || context.User.IsInRole(UserRoles.Administrator))
+ var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
+
+ // Loopback will be on LAN, so we can accept null.
+ if (ip is null || _networkManager.IsInLocalNetwork(ip))
+ {
+ context.Succeed(requirement);
+
+ return Task.CompletedTask;
+ }
+
+ if (context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
}
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
index d9c64d01c..f633c69d8 100644
--- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
{
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
deleted file mode 100644
index d772ec554..000000000
--- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.LocalAccessPolicy
-{
- /// <summary>
- /// Local access handler.
- /// </summary>
- public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public LocalAccessHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
- {
- var validated = ValidateClaims(context.User, localAccessOnly: true);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
deleted file mode 100644
index 761127fa4..000000000
--- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.LocalAccessPolicy
-{
- /// <summary>
- /// The local access authorization requirement.
- /// </summary>
- public class LocalAccessRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
deleted file mode 100644
index b235c4b63..000000000
--- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.RequiresElevationPolicy
-{
- /// <summary>
- /// Authorization handler for requiring elevated privileges.
- /// </summary>
- public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public RequiresElevationHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
- {
- var validated = ValidateClaims(context.User);
- if (validated && context.User.IsInRole(UserRoles.Administrator))
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs
deleted file mode 100644
index cfff1cc0c..000000000
--- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.RequiresElevationPolicy
-{
- /// <summary>
- /// The authorization requirement for requiring elevated privileges in the authorization handler.
- /// </summary>
- public class RequiresElevationRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
index cdd7d8a52..75ec9fcec 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
@@ -1,19 +1,17 @@
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
/// <summary>
/// Default authorization handler.
/// </summary>
- public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
+ public class SyncPlayAccessHandler : AuthorizationHandler<SyncPlayAccessRequirement>
{
private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager;
@@ -23,14 +21,9 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary>
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
+ IUserManager userManager)
{
_syncPlayManager = syncPlayManager;
_userManager = userManager;
@@ -39,27 +32,20 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement)
{
- if (!ValidateClaims(context.User))
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
var userId = context.User.GetUserId();
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
{
- if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
- || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
+ if (user.SyncPlayAccess is SyncPlayUserAccessType.CreateAndJoinGroups or SyncPlayUserAccessType.JoinGroups
|| _syncPlayManager.IsUserActive(userId))
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
{
@@ -67,10 +53,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
{
@@ -79,10 +61,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
@@ -90,14 +68,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
- }
- else
- {
- context.Fail();
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
index 6fab4c0ad..220b223b3 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
@@ -1,12 +1,12 @@
-using Jellyfin.Data.Enums;
-using Microsoft.AspNetCore.Authorization;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Data.Enums;
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
/// <summary>
/// The default authorization requirement.
/// </summary>
- public class SyncPlayAccessRequirement : IAuthorizationRequirement
+ public class SyncPlayAccessRequirement : DefaultAuthorizationRequirement
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
new file mode 100644
index 000000000..e72bec46f
--- /dev/null
+++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.UserPermissionPolicy
+{
+ /// <summary>
+ /// User permission authorization handler.
+ /// </summary>
+ public class UserPermissionHandler : AuthorizationHandler<UserPermissionRequirement>
+ {
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserPermissionHandler"/> class.
+ /// </summary>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public UserPermissionHandler(IUserManager userManager)
+ {
+ _userManager = userManager;
+ }
+
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement)
+ {
+ var user = _userManager.GetUserById(context.User.GetUserId());
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ if (user.HasPermission(requirement.RequiredPermission))
+ {
+ context.Succeed(requirement);
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs
new file mode 100644
index 000000000..4694556eb
--- /dev/null
+++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs
@@ -0,0 +1,26 @@
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Api.Auth.UserPermissionPolicy
+{
+ /// <summary>
+ /// The user permission requirement.
+ /// </summary>
+ public class UserPermissionRequirement : DefaultAuthorizationRequirement
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserPermissionRequirement"/> class.
+ /// </summary>
+ /// <param name="requiredPermission">The required <see cref="PermissionKind"/>.</param>
+ /// <param name="validateParentalSchedule">Whether to validate the user's parental schedule.</param>
+ public UserPermissionRequirement(PermissionKind requiredPermission, bool validateParentalSchedule = true) : base(validateParentalSchedule)
+ {
+ RequiredPermission = requiredPermission;
+ }
+
+ /// <summary>
+ /// Gets the required user permission.
+ /// </summary>
+ public PermissionKind RequiredPermission { get; }
+ }
+}
diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index e327831fe..5b4bd0adb 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -4,35 +4,34 @@ using Jellyfin.Api.Results;
using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api
+namespace Jellyfin.Api;
+
+/// <summary>
+/// Base api controller for the API setting a default route.
+/// </summary>
+[ApiController]
+[Route("[controller]")]
+[Produces(
+ MediaTypeNames.Application.Json,
+ JsonDefaults.CamelCaseMediaType,
+ JsonDefaults.PascalCaseMediaType)]
+public class BaseJellyfinApiController : ControllerBase
{
/// <summary>
- /// Base api controller for the API setting a default route.
+ /// Create a new <see cref="OkResult{T}"/>.
/// </summary>
- [ApiController]
- [Route("[controller]")]
- [Produces(
- MediaTypeNames.Application.Json,
- JsonDefaults.CamelCaseMediaType,
- JsonDefaults.PascalCaseMediaType)]
- public class BaseJellyfinApiController : ControllerBase
- {
- /// <summary>
- /// Create a new <see cref="OkResult{T}"/>.
- /// </summary>
- /// <param name="value">The value to return.</param>
- /// <typeparam name="T">The type to return.</typeparam>
- /// <returns>The <see cref="ActionResult{T}"/>.</returns>
- protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
- => new OkResult<IEnumerable<T>?>(value);
+ /// <param name="value">The value to return.</param>
+ /// <typeparam name="T">The type to return.</typeparam>
+ /// <returns>The <see cref="ActionResult{T}"/>.</returns>
+ protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
+ => new OkResult<IEnumerable<T>?>(value);
- /// <summary>
- /// Create a new <see cref="OkResult{T}"/>.
- /// </summary>
- /// <param name="value">The value to return.</param>
- /// <typeparam name="T">The type to return.</typeparam>
- /// <returns>The <see cref="ActionResult{T}"/>.</returns>
- protected ActionResult<T> Ok<T>(T value)
- => new OkResult<T>(value);
- }
+ /// <summary>
+ /// Create a new <see cref="OkResult{T}"/>.
+ /// </summary>
+ /// <param name="value">The value to return.</param>
+ /// <typeparam name="T">The type to return.</typeparam>
+ /// <returns>The <see cref="ActionResult{T}"/>.</returns>
+ protected ActionResult<T> Ok<T>(T value)
+ => new OkResult<T>(value);
}
diff --git a/Jellyfin.Api/Constants/AuthenticationSchemes.cs b/Jellyfin.Api/Constants/AuthenticationSchemes.cs
index bac3379e7..d5c2253e4 100644
--- a/Jellyfin.Api/Constants/AuthenticationSchemes.cs
+++ b/Jellyfin.Api/Constants/AuthenticationSchemes.cs
@@ -1,13 +1,12 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+/// <summary>
+/// Authentication schemes for user authentication in the API.
+/// </summary>
+public static class AuthenticationSchemes
{
/// <summary>
- /// Authentication schemes for user authentication in the API.
+ /// Scheme name for the custom legacy authentication.
/// </summary>
- public static class AuthenticationSchemes
- {
- /// <summary>
- /// Scheme name for the custom legacy authentication.
- /// </summary>
- public const string CustomAuthentication = "CustomAuthentication";
- }
+ public const string CustomAuthentication = "CustomAuthentication";
}
diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs
index 8323312e5..73c4acb88 100644
--- a/Jellyfin.Api/Constants/InternalClaimTypes.cs
+++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs
@@ -1,43 +1,42 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+/// <summary>
+/// Internal claim types for authorization.
+/// </summary>
+public static class InternalClaimTypes
{
/// <summary>
- /// Internal claim types for authorization.
+ /// User Id.
/// </summary>
- public static class InternalClaimTypes
- {
- /// <summary>
- /// User Id.
- /// </summary>
- public const string UserId = "Jellyfin-UserId";
+ public const string UserId = "Jellyfin-UserId";
- /// <summary>
- /// Device Id.
- /// </summary>
- public const string DeviceId = "Jellyfin-DeviceId";
+ /// <summary>
+ /// Device Id.
+ /// </summary>
+ public const string DeviceId = "Jellyfin-DeviceId";
- /// <summary>
- /// Device.
- /// </summary>
- public const string Device = "Jellyfin-Device";
+ /// <summary>
+ /// Device.
+ /// </summary>
+ public const string Device = "Jellyfin-Device";
- /// <summary>
- /// Client.
- /// </summary>
- public const string Client = "Jellyfin-Client";
+ /// <summary>
+ /// Client.
+ /// </summary>
+ public const string Client = "Jellyfin-Client";
- /// <summary>
- /// Version.
- /// </summary>
- public const string Version = "Jellyfin-Version";
+ /// <summary>
+ /// Version.
+ /// </summary>
+ public const string Version = "Jellyfin-Version";
- /// <summary>
- /// Token.
- /// </summary>
- public const string Token = "Jellyfin-Token";
+ /// <summary>
+ /// Token.
+ /// </summary>
+ public const string Token = "Jellyfin-Token";
- /// <summary>
- /// Is Api Key.
- /// </summary>
- public const string IsApiKey = "Jellyfin-IsApiKey";
- }
+ /// <summary>
+ /// Is Api Key.
+ /// </summary>
+ public const string IsApiKey = "Jellyfin-IsApiKey";
}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index a72eeea28..53841b0c4 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -1,78 +1,87 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+/// <summary>
+/// Policies for the API authorization.
+/// </summary>
+public static class Policies
{
/// <summary>
- /// Policies for the API authorization.
- /// </summary>
- public static class Policies
- {
- /// <summary>
- /// Policy name for default authorization.
- /// </summary>
- public const string DefaultAuthorization = "DefaultAuthorization";
-
- /// <summary>
- /// Policy name for requiring first time setup or elevated privileges.
- /// </summary>
- public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
-
- /// <summary>
- /// Policy name for requiring elevated privileges.
- /// </summary>
- public const string RequiresElevation = "RequiresElevation";
-
- /// <summary>
- /// Policy name for allowing local access only.
- /// </summary>
- public const string LocalAccessOnly = "LocalAccessOnly";
-
- /// <summary>
- /// Policy name for escaping schedule controls.
- /// </summary>
- public const string IgnoreParentalControl = "IgnoreParentalControl";
-
- /// <summary>
- /// Policy name for requiring download permission.
- /// </summary>
- public const string Download = "Download";
-
- /// <summary>
- /// Policy name for requiring first time setup or default permissions.
- /// </summary>
- public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
-
- /// <summary>
- /// Policy name for requiring local access or elevated privileges.
- /// </summary>
- public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
-
- /// <summary>
- /// Policy name for requiring (anonymous) LAN access.
- /// </summary>
- public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
-
- /// <summary>
- /// Policy name for escaping schedule controls or requiring first time setup.
- /// </summary>
- public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
-
- /// <summary>
- /// Policy name for accessing SyncPlay.
- /// </summary>
- public const string SyncPlayHasAccess = "SyncPlayHasAccess";
-
- /// <summary>
- /// Policy name for creating a SyncPlay group.
- /// </summary>
- public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
-
- /// <summary>
- /// Policy name for joining a SyncPlay group.
- /// </summary>
- public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
-
- /// <summary>
- /// Policy name for accessing a SyncPlay group.
- /// </summary>
- public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
- }
+ /// Policy name for requiring first time setup or elevated privileges.
+ /// </summary>
+ public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
+
+ /// <summary>
+ /// Policy name for requiring elevated privileges.
+ /// </summary>
+ public const string RequiresElevation = "RequiresElevation";
+
+ /// <summary>
+ /// Policy name for allowing local access only.
+ /// </summary>
+ public const string LocalAccessOnly = "LocalAccessOnly";
+
+ /// <summary>
+ /// Policy name for escaping schedule controls.
+ /// </summary>
+ public const string IgnoreParentalControl = "IgnoreParentalControl";
+
+ /// <summary>
+ /// Policy name for requiring download permission.
+ /// </summary>
+ public const string Download = "Download";
+
+ /// <summary>
+ /// Policy name for requiring first time setup or default permissions.
+ /// </summary>
+ public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
+
+ /// <summary>
+ /// Policy name for requiring local access or elevated privileges.
+ /// </summary>
+ public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
+
+ /// <summary>
+ /// Policy name for requiring (anonymous) LAN access.
+ /// </summary>
+ public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
+
+ /// <summary>
+ /// Policy name for escaping schedule controls or requiring first time setup.
+ /// </summary>
+ public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
+
+ /// <summary>
+ /// Policy name for accessing SyncPlay.
+ /// </summary>
+ public const string SyncPlayHasAccess = "SyncPlayHasAccess";
+
+ /// <summary>
+ /// Policy name for creating a SyncPlay group.
+ /// </summary>
+ public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
+
+ /// <summary>
+ /// Policy name for joining a SyncPlay group.
+ /// </summary>
+ public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
+
+ /// <summary>
+ /// Policy name for accessing a SyncPlay group.
+ /// </summary>
+ public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
+
+ /// <summary>
+ /// Policy name for accessing collection management.
+ /// </summary>
+ public const string CollectionManagement = "CollectionManagement";
+
+ /// <summary>
+ /// Policy name for accessing LiveTV.
+ /// </summary>
+ public const string LiveTvAccess = "LiveTvAccess";
+
+ /// <summary>
+ /// Policy name for managing LiveTV.
+ /// </summary>
+ public const string LiveTvManagement = "LiveTvManagement";
}
diff --git a/Jellyfin.Api/Constants/UserRoles.cs b/Jellyfin.Api/Constants/UserRoles.cs
index d9a536e7d..41c7b7cd0 100644
--- a/Jellyfin.Api/Constants/UserRoles.cs
+++ b/Jellyfin.Api/Constants/UserRoles.cs
@@ -1,23 +1,22 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+/// <summary>
+/// Constants for user roles used in the authentication and authorization for the API.
+/// </summary>
+public static class UserRoles
{
/// <summary>
- /// Constants for user roles used in the authentication and authorization for the API.
+ /// Guest user.
/// </summary>
- public static class UserRoles
- {
- /// <summary>
- /// Guest user.
- /// </summary>
- public const string Guest = "Guest";
+ public const string Guest = "Guest";
- /// <summary>
- /// Regular user with no special privileges.
- /// </summary>
- public const string User = "User";
+ /// <summary>
+ /// Regular user with no special privileges.
+ /// </summary>
+ public const string User = "User";
- /// <summary>
- /// Administrator user with elevated privileges.
- /// </summary>
- public const string Administrator = "Administrator";
- }
+ /// <summary>
+ /// Administrator user with elevated privileges.
+ /// </summary>
+ public const string Administrator = "Administrator";
}
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index ae45f647f..c3d02976e 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -8,50 +8,49 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Activity log controller.
+/// </summary>
+[Route("System/ActivityLog")]
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ActivityLogController : BaseJellyfinApiController
{
+ private readonly IActivityManager _activityManager;
+
/// <summary>
- /// Activity log controller.
+ /// Initializes a new instance of the <see cref="ActivityLogController"/> class.
/// </summary>
- [Route("System/ActivityLog")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ActivityLogController : BaseJellyfinApiController
+ /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
+ public ActivityLogController(IActivityManager activityManager)
{
- private readonly IActivityManager _activityManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ActivityLogController"/> class.
- /// </summary>
- /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
- public ActivityLogController(IActivityManager activityManager)
- {
- _activityManager = activityManager;
- }
+ _activityManager = activityManager;
+ }
- /// <summary>
- /// Gets activity log entries.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
- /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
- /// <response code="200">Activity log returned.</response>
- /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
- [HttpGet("Entries")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] DateTime? minDate,
- [FromQuery] bool? hasUserId)
+ /// <summary>
+ /// Gets activity log entries.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
+ /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
+ /// <response code="200">Activity log returned.</response>
+ /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
+ [HttpGet("Entries")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] DateTime? minDate,
+ [FromQuery] bool? hasUserId)
+ {
+ return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
{
- return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
- {
- Skip = startIndex,
- Limit = limit,
- MinDate = minDate,
- HasUserId = hasUserId
- }).ConfigureAwait(false);
- }
+ Skip = startIndex,
+ Limit = limit,
+ MinDate = minDate,
+ HasUserId = hasUserId
+ }).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 024a15349..991f8cbf2 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -7,70 +7,69 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Authentication controller.
+/// </summary>
+[Route("Auth")]
+public class ApiKeyController : BaseJellyfinApiController
{
+ private readonly IAuthenticationManager _authenticationManager;
+
/// <summary>
- /// Authentication controller.
+ /// Initializes a new instance of the <see cref="ApiKeyController"/> class.
/// </summary>
- [Route("Auth")]
- public class ApiKeyController : BaseJellyfinApiController
+ /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
+ public ApiKeyController(IAuthenticationManager authenticationManager)
{
- private readonly IAuthenticationManager _authenticationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ApiKeyController"/> class.
- /// </summary>
- /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
- public ApiKeyController(IAuthenticationManager authenticationManager)
- {
- _authenticationManager = authenticationManager;
- }
+ _authenticationManager = authenticationManager;
+ }
- /// <summary>
- /// Get all keys.
- /// </summary>
- /// <response code="200">Api keys retrieved.</response>
- /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
- [HttpGet("Keys")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
- {
- var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
+ /// <summary>
+ /// Get all keys.
+ /// </summary>
+ /// <response code="200">Api keys retrieved.</response>
+ /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
+ [HttpGet("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
+ {
+ var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
- return new QueryResult<AuthenticationInfo>(keys);
- }
+ return new QueryResult<AuthenticationInfo>(keys);
+ }
- /// <summary>
- /// Create a new api key.
- /// </summary>
- /// <param name="app">Name of the app using the authentication key.</param>
- /// <response code="204">Api key created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Keys")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
- {
- await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
+ /// <summary>
+ /// Create a new api key.
+ /// </summary>
+ /// <param name="app">Name of the app using the authentication key.</param>
+ /// <response code="204">Api key created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
+ {
+ await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
+ }
- /// <summary>
- /// Remove an api key.
- /// </summary>
- /// <param name="key">The access token to delete.</param>
- /// <response code="204">Api key deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Keys/{key}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
- {
- await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
+ /// <summary>
+ /// Remove an api key.
+ /// </summary>
+ /// <param name="key">The access token to delete.</param>
+ /// <response code="204">Api key deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Keys/{key}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
+ {
+ await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index c8ac2ed52..c9d2f67f9 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -17,464 +16,466 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The artists controller.
+/// </summary>
+[Route("Artists")]
+[Authorize]
+public class ArtistsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// The artists controller.
+ /// Initializes a new instance of the <see cref="ArtistsController"/> class.
/// </summary>
- [Route("Artists")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ArtistsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public ArtistsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ArtistsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public ArtistsController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
+
+ /// <summary>
+ /// Gets all artists from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</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="searchTerm">Optional. Search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
+ /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+ /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
+ /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="userId">User id.</param>
+ /// <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>
+ /// <returns>An <see cref="OkResult"/> containing the artists.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetArtists(
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [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(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery] Guid? userId,
+ [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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+
+ if (!userId.Value.Equals(default))
{
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
+ user = _userManager.GetUserById(userId.Value);
}
- /// <summary>
- /// Gets all artists from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</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="searchTerm">Optional. Search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
- /// <param name="userId">User id.</param>
- /// <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>
- /// <returns>An <see cref="OkResult"/> containing the artists.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetArtists(
- [FromQuery] double? minCommunityRating,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [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(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery] Guid? userId,
- [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)
+ var query = new InternalItemsQuery(user)
{
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
+ Person = person,
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
+ };
- User? user = null;
- BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
-
- if (userId.HasValue && !userId.Equals(default))
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- user = _userManager.GetUserById(userId.Value);
+ query.AncestorIds = new[] { parentId.Value };
}
-
- var query = new InternalItemsQuery(user)
+ else
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = tags,
- OfficialRatings = officialRatings,
- Genres = genres,
- GenreIds = genreIds,
- StudioIds = studioIds,
- Person = person,
- PersonIds = personIds,
- PersonTypes = personTypes,
- Years = years,
- MinCommunityRating = minCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
+ query.ItemIds = new[] { parentId.Value };
+ }
+ }
+
+ // Studios
+ if (studios.Length != 0)
+ {
+ query.StudioIds = studios.Select(i =>
{
- if (parentItem is Folder)
+ try
{
- query.AncestorIds = new[] { parentId.Value };
+ return _libraryManager.GetStudio(i);
}
- else
+ catch
{
- query.ItemIds = new[] { parentId.Value };
+ return null;
}
- }
+ }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ }
- // Studios
- if (studios.Length != 0)
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- query.StudioIds = studios.Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
}
+ }
- foreach (var filter in filters)
+ var result = _libraryManager.GetArtists(query);
+
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, itemCounts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (includeItemTypes.Length != 0)
{
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
+ dto.ChildCount = itemCounts.ItemCount;
+ dto.ProgramCount = itemCounts.ProgramCount;
+ dto.SeriesCount = itemCounts.SeriesCount;
+ dto.EpisodeCount = itemCounts.EpisodeCount;
+ dto.MovieCount = itemCounts.MovieCount;
+ dto.TrailerCount = itemCounts.TrailerCount;
+ dto.AlbumCount = itemCounts.AlbumCount;
+ dto.SongCount = itemCounts.SongCount;
+ dto.ArtistCount = itemCounts.ArtistCount;
}
- var result = _libraryManager.GetArtists(query);
+ return dto;
+ });
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, itemCounts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+ return new QueryResult<BaseItemDto>(
+ query.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
+ }
- if (includeItemTypes.Length != 0)
- {
- dto.ChildCount = itemCounts.ItemCount;
- dto.ProgramCount = itemCounts.ProgramCount;
- dto.SeriesCount = itemCounts.SeriesCount;
- dto.EpisodeCount = itemCounts.EpisodeCount;
- dto.MovieCount = itemCounts.MovieCount;
- dto.TrailerCount = itemCounts.TrailerCount;
- dto.AlbumCount = itemCounts.AlbumCount;
- dto.SongCount = itemCounts.SongCount;
- dto.ArtistCount = itemCounts.ArtistCount;
- }
+ /// <summary>
+ /// Gets all album artists from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</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="searchTerm">Optional. Search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
+ /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+ /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
+ /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="userId">User id.</param>
+ /// <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>
+ /// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
+ [HttpGet("AlbumArtists")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [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(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery] Guid? userId,
+ [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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- return dto;
- });
+ User? user = null;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
- return new QueryResult<BaseItemDto>(
- query.StartIndex,
- result.TotalRecordCount,
- dtos.ToArray());
+ if (!userId.Value.Equals(default))
+ {
+ user = _userManager.GetUserById(userId.Value);
}
- /// <summary>
- /// Gets all album artists from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</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="searchTerm">Optional. Search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
- /// <param name="userId">User id.</param>
- /// <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>
- /// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
- [HttpGet("AlbumArtists")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
- [FromQuery] double? minCommunityRating,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [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(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery] Guid? userId,
- [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)
+ var query = new InternalItemsQuery(user)
{
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- User? user = null;
- BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
+ Person = person,
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
+ };
- if (userId.HasValue && !userId.Equals(default))
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- user = _userManager.GetUserById(userId.Value);
+ query.AncestorIds = new[] { parentId.Value };
}
-
- var query = new InternalItemsQuery(user)
+ else
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = tags,
- OfficialRatings = officialRatings,
- Genres = genres,
- GenreIds = genreIds,
- StudioIds = studioIds,
- Person = person,
- PersonIds = personIds,
- PersonTypes = personTypes,
- Years = years,
- MinCommunityRating = minCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
+ query.ItemIds = new[] { parentId.Value };
+ }
+ }
+
+ // Studios
+ if (studios.Length != 0)
+ {
+ query.StudioIds = studios.Select(i =>
{
- if (parentItem is Folder)
+ try
{
- query.AncestorIds = new[] { parentId.Value };
+ return _libraryManager.GetStudio(i);
}
- else
+ catch
{
- query.ItemIds = new[] { parentId.Value };
+ return null;
}
- }
+ }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ }
- // Studios
- if (studios.Length != 0)
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- query.StudioIds = studios.Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
}
+ }
- foreach (var filter in filters)
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
+ var result = _libraryManager.GetAlbumArtists(query);
- var result = _libraryManager.GetAlbumArtists(query);
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, itemCounts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- var dtos = result.Items.Select(i =>
+ if (includeItemTypes.Length != 0)
{
- var (baseItem, itemCounts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (includeItemTypes.Length != 0)
- {
- dto.ChildCount = itemCounts.ItemCount;
- dto.ProgramCount = itemCounts.ProgramCount;
- dto.SeriesCount = itemCounts.SeriesCount;
- dto.EpisodeCount = itemCounts.EpisodeCount;
- dto.MovieCount = itemCounts.MovieCount;
- dto.TrailerCount = itemCounts.TrailerCount;
- dto.AlbumCount = itemCounts.AlbumCount;
- dto.SongCount = itemCounts.SongCount;
- dto.ArtistCount = itemCounts.ArtistCount;
- }
-
- return dto;
- });
+ dto.ChildCount = itemCounts.ItemCount;
+ dto.ProgramCount = itemCounts.ProgramCount;
+ dto.SeriesCount = itemCounts.SeriesCount;
+ dto.EpisodeCount = itemCounts.EpisodeCount;
+ dto.MovieCount = itemCounts.MovieCount;
+ dto.TrailerCount = itemCounts.TrailerCount;
+ dto.AlbumCount = itemCounts.AlbumCount;
+ dto.SongCount = itemCounts.SongCount;
+ dto.ArtistCount = itemCounts.ArtistCount;
+ }
- return new QueryResult<BaseItemDto>(
- query.StartIndex,
- result.TotalRecordCount,
- dtos.ToArray());
- }
+ return dto;
+ });
- /// <summary>
- /// Gets an artist by name.
- /// </summary>
- /// <param name="name">Studio name.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Artist returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the artist.</returns>
- [HttpGet("{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
- {
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ return new QueryResult<BaseItemDto>(
+ query.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
+ }
- var item = _libraryManager.GetArtist(name, dtoOptions);
+ /// <summary>
+ /// Gets an artist by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Artist returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the artist.</returns>
+ [HttpGet("{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- if (userId.HasValue && !userId.Value.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetArtist(name, dtoOptions);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ if (!userId.Value.Equals(default))
+ {
+ var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 94f7a7b82..968193a6f 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -10,355 +10,354 @@ using MediaBrowser.Model.Dlna;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The audio controller.
+/// </summary>
+// TODO: In order to authenticate this in the future, Dlna playback will require updating
+public class AudioController : BaseJellyfinApiController
{
+ private readonly AudioHelper _audioHelper;
+
+ private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
+
/// <summary>
- /// The audio controller.
+ /// Initializes a new instance of the <see cref="AudioController"/> class.
/// </summary>
- // TODO: In order to authenticate this in the future, Dlna playback will require updating
- public class AudioController : BaseJellyfinApiController
+ /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+ public AudioController(AudioHelper audioHelper)
{
- private readonly AudioHelper _audioHelper;
-
- private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="AudioController"/> class.
- /// </summary>
- /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
- public AudioController(AudioHelper audioHelper)
- {
- _audioHelper = audioHelper;
- }
+ _audioHelper = audioHelper;
+ }
- /// <summary>
- /// Gets an audio stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The audio container.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <response code="200">Audio stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
- [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- public async Task<ActionResult> GetAudioStream(
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string>? streamOptions)
+ /// <summary>
+ /// Gets an audio stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The audio container.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
+ [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ public async Task<ActionResult> GetAudioStream(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string>? streamOptions)
+ {
+ StreamingRequestDto streamingRequest = new StreamingRequestDto
{
- StreamingRequestDto streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Static,
+ StreamOptions = streamOptions
+ };
- return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
- }
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets an audio stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The audio container.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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="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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <response code="200">Audio stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
- [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- public async Task<ActionResult> GetAudioStreamByContainer(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string container,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string>? streamOptions)
+ /// <summary>
+ /// Gets an audio stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The audio container.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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="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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ public async Task<ActionResult> GetAudioStreamByContainer(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string>? streamOptions)
+ {
+ StreamingRequestDto streamingRequest = new StreamingRequestDto
{
- StreamingRequestDto streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Static,
+ StreamOptions = streamOptions
+ };
- return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
- }
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
index d3ea41201..3c2c4b4db 100644
--- a/Jellyfin.Api/Controllers/BrandingController.cs
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -4,54 +4,53 @@ using MediaBrowser.Model.Branding;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Branding controller.
+/// </summary>
+public class BrandingController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Branding controller.
+ /// Initializes a new instance of the <see cref="BrandingController"/> class.
/// </summary>
- public class BrandingController : BaseJellyfinApiController
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public BrandingController(IServerConfigurationManager serverConfigurationManager)
{
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BrandingController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public BrandingController(IServerConfigurationManager serverConfigurationManager)
- {
- _serverConfigurationManager = serverConfigurationManager;
- }
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <summary>
- /// Gets branding configuration.
- /// </summary>
- /// <response code="200">Branding configuration returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
- [HttpGet("Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BrandingOptions> GetBrandingOptions()
- {
- return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- }
+ /// <summary>
+ /// Gets branding configuration.
+ /// </summary>
+ /// <response code="200">Branding configuration returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
+ [HttpGet("Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<BrandingOptions> GetBrandingOptions()
+ {
+ return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ }
- /// <summary>
- /// Gets branding css.
- /// </summary>
- /// <response code="200">Branding css returned.</response>
- /// <response code="204">No branding css configured.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the branding css if exist,
- /// or a <see cref="NoContentResult"/> if the css is not configured.
- /// </returns>
- [HttpGet("Css")]
- [HttpGet("Css.css", Name = "GetBrandingCss_2")]
- [Produces("text/css")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult<string> GetBrandingCss()
- {
- var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- return options.CustomCss ?? string.Empty;
- }
+ /// <summary>
+ /// Gets branding css.
+ /// </summary>
+ /// <response code="200">Branding css returned.</response>
+ /// <response code="204">No branding css configured.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the branding css if exist,
+ /// or a <see cref="NoContentResult"/> if the css is not configured.
+ /// </returns>
+ [HttpGet("Css")]
+ [HttpGet("Css.css", Name = "GetBrandingCss_2")]
+ [Produces("text/css")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult<string> GetBrandingCss()
+ {
+ var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ return options.CustomCss ?? string.Empty;
}
}
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index d5b589a3f..11c4ac376 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -18,234 +17,236 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Channels Controller.
+/// </summary>
+[Authorize]
+public class ChannelsController : BaseJellyfinApiController
{
+ private readonly IChannelManager _channelManager;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// Channels Controller.
+ /// Initializes a new instance of the <see cref="ChannelsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ChannelsController : BaseJellyfinApiController
+ /// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public ChannelsController(IChannelManager channelManager, IUserManager userManager)
{
- private readonly IChannelManager _channelManager;
- private readonly IUserManager _userManager;
+ _channelManager = channelManager;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ChannelsController"/> class.
- /// </summary>
- /// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public ChannelsController(IChannelManager channelManager, IUserManager userManager)
+ /// <summary>
+ /// Gets available channels.
+ /// </summary>
+ /// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</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="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
+ /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
+ /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
+ /// <response code="200">Channels returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channels.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannels(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? supportsLatestItems,
+ [FromQuery] bool? supportsMediaDeletion,
+ [FromQuery] bool? isFavorite)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ return await _channelManager.GetChannelsAsync(new ChannelQuery
{
- _channelManager = channelManager;
- _userManager = userManager;
- }
+ Limit = limit,
+ StartIndex = startIndex,
+ UserId = userId.Value,
+ SupportsLatestItems = supportsLatestItems,
+ SupportsMediaDeletion = supportsMediaDeletion,
+ IsFavorite = isFavorite
+ }).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets available channels.
- /// </summary>
- /// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</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="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
- /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
- /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
- /// <response code="200">Channels returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channels.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetChannels(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? supportsLatestItems,
- [FromQuery] bool? supportsMediaDeletion,
- [FromQuery] bool? isFavorite)
- {
- return _channelManager.GetChannels(new ChannelQuery
- {
- Limit = limit,
- StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
- SupportsLatestItems = supportsLatestItems,
- SupportsMediaDeletion = supportsMediaDeletion,
- IsFavorite = isFavorite
- });
- }
+ /// <summary>
+ /// Get all channel features.
+ /// </summary>
+ /// <response code="200">All channel features returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
+ [HttpGet("Features")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
+ {
+ return _channelManager.GetAllChannelFeatures();
+ }
- /// <summary>
- /// Get all channel features.
- /// </summary>
- /// <response code="200">All channel features returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
- [HttpGet("Features")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
- {
- return _channelManager.GetAllChannelFeatures();
- }
+ /// <summary>
+ /// Get channel features.
+ /// </summary>
+ /// <param name="channelId">Channel id.</param>
+ /// <response code="200">Channel features returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
+ [HttpGet("{channelId}/Features")]
+ public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
+ {
+ return _channelManager.GetChannelFeatures(channelId);
+ }
- /// <summary>
- /// Get channel features.
- /// </summary>
- /// <param name="channelId">Channel id.</param>
- /// <response code="200">Channel features returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
- [HttpGet("{channelId}/Features")]
- public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
- {
- return _channelManager.GetChannelFeatures(channelId);
- }
+ /// <summary>
+ /// Get channel items.
+ /// </summary>
+ /// <param name="channelId">Channel Id.</param>
+ /// <param name="folderId">Optional. Folder Id.</param>
+ /// <param name="userId">Optional. 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="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <response code="200">Channel items returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> representing the request to get the channel items.
+ /// The task result contains an <see cref="OkResult"/> containing the channel items.
+ /// </returns>
+ [HttpGet("{channelId}/Items")]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
+ [FromRoute, Required] Guid channelId,
+ [FromQuery] Guid? folderId,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <summary>
- /// Get channel items.
- /// </summary>
- /// <param name="channelId">Channel Id.</param>
- /// <param name="folderId">Optional. Folder Id.</param>
- /// <param name="userId">Optional. 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="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <response code="200">Channel items returned.</response>
- /// <returns>
- /// A <see cref="Task"/> representing the request to get the channel items.
- /// The task result contains an <see cref="OkResult"/> containing the channel items.
- /// </returns>
- [HttpGet("{channelId}/Items")]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
- [FromRoute, Required] Guid channelId,
- [FromQuery] Guid? folderId,
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
+ var query = new InternalItemsQuery(user)
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ Limit = limit,
+ StartIndex = startIndex,
+ ChannelIds = new[] { channelId },
+ ParentId = folderId ?? Guid.Empty,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+ DtoOptions = new DtoOptions { Fields = fields }
+ };
- var query = new InternalItemsQuery(user)
- {
- Limit = limit,
- StartIndex = startIndex,
- ChannelIds = new[] { channelId },
- ParentId = folderId ?? Guid.Empty,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
- DtoOptions = new DtoOptions { Fields = fields }
- };
-
- foreach (var filter in filters)
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- switch (filter)
- {
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- }
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
}
-
- return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
- /// <summary>
- /// Gets latest channel items.
- /// </summary>
- /// <param name="userId">Optional. 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="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
- /// <response code="200">Latest channel items returned.</response>
- /// <returns>
- /// A <see cref="Task"/> representing the request to get the latest channel items.
- /// The task result contains an <see cref="OkResult"/> containing the latest channel items.
- /// </returns>
- [HttpGet("Items/Latest")]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
+ }
- var query = new InternalItemsQuery(user)
- {
- Limit = limit,
- StartIndex = startIndex,
- ChannelIds = channelIds,
- DtoOptions = new DtoOptions { Fields = fields }
- };
+ /// <summary>
+ /// Gets latest channel items.
+ /// </summary>
+ /// <param name="userId">Optional. 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="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
+ /// <response code="200">Latest channel items returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> representing the request to get the latest channel items.
+ /// The task result contains an <see cref="OkResult"/> containing the latest channel items.
+ /// </returns>
+ [HttpGet("Items/Latest")]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- foreach (var filter in filters)
+ var query = new InternalItemsQuery(user)
+ {
+ Limit = limit,
+ StartIndex = startIndex,
+ ChannelIds = channelIds,
+ DtoOptions = new DtoOptions { Fields = fields }
+ };
+
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- switch (filter)
- {
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- }
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
}
-
- return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
+
+ return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs
index ed073a687..2c5dbacbb 100644
--- a/Jellyfin.Api/Controllers/ClientLogController.cs
+++ b/Jellyfin.Api/Controllers/ClientLogController.cs
@@ -1,9 +1,7 @@
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.ClientLogDtos;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Configuration;
@@ -11,71 +9,70 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Client log controller.
+/// </summary>
+[Authorize]
+public class ClientLogController : BaseJellyfinApiController
{
+ private const int MaxDocumentSize = 1_000_000;
+ private readonly IClientEventLogger _clientEventLogger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Client log controller.
+ /// Initializes a new instance of the <see cref="ClientLogController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ClientLogController : BaseJellyfinApiController
+ /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ClientLogController(
+ IClientEventLogger clientEventLogger,
+ IServerConfigurationManager serverConfigurationManager)
{
- private const int MaxDocumentSize = 1_000_000;
- private readonly IClientEventLogger _clientEventLogger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
+ _clientEventLogger = clientEventLogger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ClientLogController"/> class.
- /// </summary>
- /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public ClientLogController(
- IClientEventLogger clientEventLogger,
- IServerConfigurationManager serverConfigurationManager)
+ /// <summary>
+ /// Upload a document.
+ /// </summary>
+ /// <response code="200">Document saved.</response>
+ /// <response code="403">Event logging disabled.</response>
+ /// <response code="413">Upload size too large.</response>
+ /// <returns>Create response.</returns>
+ [HttpPost("Document")]
+ [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
+ [AcceptsFile(MediaTypeNames.Text.Plain)]
+ [RequestSizeLimit(MaxDocumentSize)]
+ public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile()
+ {
+ if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
{
- _clientEventLogger = clientEventLogger;
- _serverConfigurationManager = serverConfigurationManager;
+ return Forbid();
}
- /// <summary>
- /// Upload a document.
- /// </summary>
- /// <response code="200">Document saved.</response>
- /// <response code="403">Event logging disabled.</response>
- /// <response code="413">Upload size too large.</response>
- /// <returns>Create response.</returns>
- [HttpPost("Document")]
- [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
- [AcceptsFile(MediaTypeNames.Text.Plain)]
- [RequestSizeLimit(MaxDocumentSize)]
- public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile()
+ if (Request.ContentLength > MaxDocumentSize)
{
- if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
- {
- return Forbid();
- }
-
- if (Request.ContentLength > MaxDocumentSize)
- {
- // Manually validate to return proper status code.
- return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
- }
-
- var (clientName, clientVersion) = GetRequestInformation();
- var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
- .ConfigureAwait(false);
- return Ok(new ClientLogDocumentResponseDto(fileName));
+ // Manually validate to return proper status code.
+ return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
}
- private (string ClientName, string ClientVersion) GetRequestInformation()
- {
- var clientName = HttpContext.User.GetClient() ?? "unknown-client";
- var clientVersion = HttpContext.User.GetIsApiKey()
- ? "apikey"
- : HttpContext.User.GetVersion() ?? "unknown-version";
+ var (clientName, clientVersion) = GetRequestInformation();
+ var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
+ .ConfigureAwait(false);
+ return Ok(new ClientLogDocumentResponseDto(fileName));
+ }
- return (clientName, clientVersion);
- }
+ private (string ClientName, string ClientVersion) GetRequestInformation()
+ {
+ var clientName = HttpContext.User.GetClient() ?? "unknown-client";
+ var clientVersion = HttpContext.User.GetIsApiKey()
+ ? "apikey"
+ : HttpContext.User.GetVersion() ?? "unknown-version";
+
+ return (clientName, clientVersion);
}
}
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index effc9ed7a..2db04afb8 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -11,101 +11,100 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The collection controller.
+/// </summary>
+[Route("Collections")]
+[Authorize(Policy = Policies.CollectionManagement)]
+public class CollectionController : BaseJellyfinApiController
{
+ private readonly ICollectionManager _collectionManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// The collection controller.
+ /// Initializes a new instance of the <see cref="CollectionController"/> class.
/// </summary>
- [Route("Collections")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class CollectionController : BaseJellyfinApiController
+ /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ public CollectionController(
+ ICollectionManager collectionManager,
+ IDtoService dtoService)
{
- private readonly ICollectionManager _collectionManager;
- private readonly IDtoService _dtoService;
+ _collectionManager = collectionManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="CollectionController"/> class.
- /// </summary>
- /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
- /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
- public CollectionController(
- ICollectionManager collectionManager,
- IDtoService dtoService)
- {
- _collectionManager = collectionManager;
- _dtoService = dtoService;
- }
+ /// <summary>
+ /// Creates a new collection.
+ /// </summary>
+ /// <param name="name">The name of the collection.</param>
+ /// <param name="ids">Item Ids to add to the collection.</param>
+ /// <param name="parentId">Optional. Create the collection within a specific folder.</param>
+ /// <param name="isLocked">Whether or not to lock the new collection.</param>
+ /// <response code="200">Collection created.</response>
+ /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
+ [FromQuery] string? name,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool isLocked = false)
+ {
+ var userId = User.GetUserId();
- /// <summary>
- /// Creates a new collection.
- /// </summary>
- /// <param name="name">The name of the collection.</param>
- /// <param name="ids">Item Ids to add to the collection.</param>
- /// <param name="parentId">Optional. Create the collection within a specific folder.</param>
- /// <param name="isLocked">Whether or not to lock the new collection.</param>
- /// <response code="200">Collection created.</response>
- /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
- [FromQuery] string? name,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
- [FromQuery] Guid? parentId,
- [FromQuery] bool isLocked = false)
+ var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
- var userId = User.GetUserId();
-
- var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
- {
- IsLocked = isLocked,
- Name = name,
- ParentId = parentId,
- ItemIdList = ids,
- UserIds = new[] { userId }
- }).ConfigureAwait(false);
+ IsLocked = isLocked,
+ Name = name,
+ ParentId = parentId,
+ ItemIdList = ids,
+ UserIds = new[] { userId }
+ }).ConfigureAwait(false);
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
+ var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
- return new CollectionCreationResult
- {
- Id = dto.Id
- };
- }
-
- /// <summary>
- /// Adds items to a collection.
- /// </summary>
- /// <param name="collectionId">The collection id.</param>
- /// <param name="ids">Item ids, comma delimited.</param>
- /// <response code="204">Items added to collection.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("{collectionId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToCollection(
- [FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ return new CollectionCreationResult
{
- await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
- return NoContent();
- }
+ Id = dto.Id
+ };
+ }
- /// <summary>
- /// Removes items from a collection.
- /// </summary>
- /// <param name="collectionId">The collection id.</param>
- /// <param name="ids">Item ids, comma delimited.</param>
- /// <response code="204">Items removed from collection.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpDelete("{collectionId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromCollection(
- [FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
- {
- await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Adds items to a collection.
+ /// </summary>
+ /// <param name="collectionId">The collection id.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
+ /// <response code="204">Items added to collection.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("{collectionId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> AddToCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Removes items from a collection.
+ /// </summary>
+ /// <param name="collectionId">The collection id.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
+ /// <response code="204">Items removed from collection.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpDelete("{collectionId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RemoveFromCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index a00ac1b0a..9007dfc41 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -13,124 +13,123 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Configuration Controller.
+/// </summary>
+[Route("System")]
+[Authorize]
+public class ConfigurationController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+
+ private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
+
/// <summary>
- /// Configuration Controller.
+ /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
/// </summary>
- [Route("System")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ConfigurationController : BaseJellyfinApiController
+ /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ public ConfigurationController(
+ IServerConfigurationManager configurationManager,
+ IMediaEncoder mediaEncoder)
{
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IMediaEncoder _mediaEncoder;
+ _configurationManager = configurationManager;
+ _mediaEncoder = mediaEncoder;
+ }
- private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
+ /// <summary>
+ /// Gets application configuration.
+ /// </summary>
+ /// <response code="200">Application configuration returned.</response>
+ /// <returns>Application configuration.</returns>
+ [HttpGet("Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<ServerConfiguration> GetConfiguration()
+ {
+ return _configurationManager.Configuration;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
- /// </summary>
- /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- public ConfigurationController(
- IServerConfigurationManager configurationManager,
- IMediaEncoder mediaEncoder)
- {
- _configurationManager = configurationManager;
- _mediaEncoder = mediaEncoder;
- }
+ /// <summary>
+ /// Updates application configuration.
+ /// </summary>
+ /// <param name="configuration">Configuration.</param>
+ /// <response code="204">Configuration updated.</response>
+ /// <returns>Update status.</returns>
+ [HttpPost("Configuration")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
+ {
+ _configurationManager.ReplaceConfiguration(configuration);
+ return NoContent();
+ }
- /// <summary>
- /// Gets application configuration.
- /// </summary>
- /// <response code="200">Application configuration returned.</response>
- /// <returns>Application configuration.</returns>
- [HttpGet("Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<ServerConfiguration> GetConfiguration()
- {
- return _configurationManager.Configuration;
- }
+ /// <summary>
+ /// Gets a named configuration.
+ /// </summary>
+ /// <param name="key">Configuration key.</param>
+ /// <response code="200">Configuration returned.</response>
+ /// <returns>Configuration.</returns>
+ [HttpGet("Configuration/{key}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Application.Json)]
+ public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
+ {
+ return _configurationManager.GetConfiguration(key);
+ }
- /// <summary>
- /// Updates application configuration.
- /// </summary>
- /// <param name="configuration">Configuration.</param>
- /// <response code="204">Configuration updated.</response>
- /// <returns>Update status.</returns>
- [HttpPost("Configuration")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
- {
- _configurationManager.ReplaceConfiguration(configuration);
- return NoContent();
- }
+ /// <summary>
+ /// Updates named configuration.
+ /// </summary>
+ /// <param name="key">Configuration key.</param>
+ /// <param name="configuration">Configuration.</param>
+ /// <response code="204">Named configuration updated.</response>
+ /// <returns>Update status.</returns>
+ [HttpPost("Configuration/{key}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration)
+ {
+ var configurationType = _configurationManager.GetConfigurationType(key);
+ var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions);
- /// <summary>
- /// Gets a named configuration.
- /// </summary>
- /// <param name="key">Configuration key.</param>
- /// <response code="200">Configuration returned.</response>
- /// <returns>Configuration.</returns>
- [HttpGet("Configuration/{key}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile(MediaTypeNames.Application.Json)]
- public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
+ if (deserializedConfiguration is null)
{
- return _configurationManager.GetConfiguration(key);
+ throw new ArgumentException("Body doesn't contain a valid configuration");
}
- /// <summary>
- /// Updates named configuration.
- /// </summary>
- /// <param name="key">Configuration key.</param>
- /// <param name="configuration">Configuration.</param>
- /// <response code="204">Named configuration updated.</response>
- /// <returns>Update status.</returns>
- [HttpPost("Configuration/{key}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration)
- {
- var configurationType = _configurationManager.GetConfigurationType(key);
- var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions);
-
- if (deserializedConfiguration is null)
- {
- throw new ArgumentException("Body doesn't contain a valid configuration");
- }
-
- _configurationManager.SaveConfiguration(key, deserializedConfiguration);
- return NoContent();
- }
+ _configurationManager.SaveConfiguration(key, deserializedConfiguration);
+ return NoContent();
+ }
- /// <summary>
- /// Gets a default MetadataOptions object.
- /// </summary>
- /// <response code="200">Metadata options returned.</response>
- /// <returns>Default MetadataOptions.</returns>
- [HttpGet("Configuration/MetadataOptions/Default")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
- {
- return new MetadataOptions();
- }
+ /// <summary>
+ /// Gets a default MetadataOptions object.
+ /// </summary>
+ /// <response code="200">Metadata options returned.</response>
+ /// <returns>Default MetadataOptions.</returns>
+ [HttpGet("Configuration/MetadataOptions/Default")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
+ {
+ return new MetadataOptions();
+ }
- /// <summary>
- /// Updates the path to the media encoder.
- /// </summary>
- /// <param name="mediaEncoderPath">Media encoder path form body.</param>
- /// <response code="204">Media encoder path updated.</response>
- /// <returns>Status.</returns>
- [HttpPost("MediaEncoder/Path")]
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
- {
- _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
- return NoContent();
- }
+ /// <summary>
+ /// Updates the path to the media encoder.
+ /// </summary>
+ /// <param name="mediaEncoderPath">Media encoder path form body.</param>
+ /// <response code="204">Media encoder path updated.</response>
+ /// <returns>Status.</returns>
+ [HttpPost("MediaEncoder/Path")]
+ [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
+ {
+ _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 3894e6c5f..076084c7a 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net;
@@ -14,103 +13,102 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The dashboard controller.
+/// </summary>
+[Route("")]
+public class DashboardController : BaseJellyfinApiController
{
+ private readonly ILogger<DashboardController> _logger;
+ private readonly IPluginManager _pluginManager;
+
/// <summary>
- /// The dashboard controller.
+ /// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary>
- [Route("")]
- public class DashboardController : BaseJellyfinApiController
+ /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
+ /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
+ public DashboardController(
+ ILogger<DashboardController> logger,
+ IPluginManager pluginManager)
{
- private readonly ILogger<DashboardController> _logger;
- private readonly IPluginManager _pluginManager;
+ _logger = logger;
+ _pluginManager = 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="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
- public DashboardController(
- ILogger<DashboardController> logger,
- IPluginManager pluginManager)
- {
- _logger = logger;
- _pluginManager = pluginManager;
- }
+ /// <summary>
+ /// Gets the configuration pages.
+ /// </summary>
+ /// <param name="enableInMainMenu">Whether to enable in the main menu.</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)]
+ [Authorize]
+ public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
+ [FromQuery] bool? enableInMainMenu)
+ {
+ var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
- /// <summary>
- /// Gets the configuration pages.
- /// </summary>
- /// <param name="enableInMainMenu">Whether to enable in the main menu.</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)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
- [FromQuery] bool? enableInMainMenu)
+ if (enableInMainMenu.HasValue)
{
- var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
-
- if (enableInMainMenu.HasValue)
- {
- configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
- }
-
- return configPages;
+ configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
}
- /// <summary>
- /// Gets a dashboard configuration page.
- /// </summary>
- /// <param name="name">The name of the page.</param>
- /// <response code="200">ConfigurationPage returned.</response>
- /// <response code="404">Plugin configuration page not found.</response>
- /// <returns>The configuration page.</returns>
- [HttpGet("web/ConfigurationPage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
- public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
- {
- var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
- if (altPage is null)
- {
- return NotFound();
- }
-
- IPlugin plugin = altPage.Item2;
- string resourcePath = altPage.Item1.EmbeddedResourcePath;
- Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
- if (stream is null)
- {
- _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
- return NotFound();
- }
+ return configPages;
+ }
- return File(stream, MimeTypes.GetMimeType(resourcePath));
+ /// <summary>
+ /// Gets a dashboard configuration page.
+ /// </summary>
+ /// <param name="name">The name of the page.</param>
+ /// <response code="200">ConfigurationPage returned.</response>
+ /// <response code="404">Plugin configuration page not found.</response>
+ /// <returns>The configuration page.</returns>
+ [HttpGet("web/ConfigurationPage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
+ public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
+ {
+ var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
+ if (altPage is null)
+ {
+ return NotFound();
}
- private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
+ IPlugin plugin = altPage.Item2;
+ string resourcePath = altPage.Item1.EmbeddedResourcePath;
+ Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
+ if (stream is null)
{
- return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
+ _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
+ return NotFound();
}
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
- {
- if (plugin.Instance is not IHasWebPages hasWebPages)
- {
- return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
- }
+ return File(stream, MimeTypes.GetMimeType(resourcePath));
+ }
- return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
- }
+ private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
+ {
+ return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
+ }
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
+ private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
+ {
+ if (plugin.Instance is not IHasWebPages hasWebPages)
{
- return _pluginManager.Plugins.SelectMany(GetPluginPages);
+ return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
}
+
+ return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
+ }
+
+ private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
+ {
+ return _pluginManager.Plugins.SelectMany(GetPluginPages);
}
}
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index aad60cf5c..aa0dff212 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries;
@@ -13,129 +14,129 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Devices Controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class DevicesController : BaseJellyfinApiController
{
+ private readonly IDeviceManager _deviceManager;
+ private readonly ISessionManager _sessionManager;
+
/// <summary>
- /// Devices Controller.
+ /// Initializes a new instance of the <see cref="DevicesController"/> class.
/// </summary>
- [Authorize(Policy = Policies.RequiresElevation)]
- public class DevicesController : BaseJellyfinApiController
+ /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+ public DevicesController(
+ IDeviceManager deviceManager,
+ ISessionManager sessionManager)
{
- private readonly IDeviceManager _deviceManager;
- private readonly ISessionManager _sessionManager;
+ _deviceManager = deviceManager;
+ _sessionManager = sessionManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="DevicesController"/> class.
- /// </summary>
- /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
- /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
- public DevicesController(
- IDeviceManager deviceManager,
- ISessionManager sessionManager)
- {
- _deviceManager = deviceManager;
- _sessionManager = sessionManager;
- }
+ /// <summary>
+ /// Get Devices.
+ /// </summary>
+ /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
+ /// <param name="userId">Gets or sets the user identifier.</param>
+ /// <response code="200">Devices retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
+ }
- /// <summary>
- /// Get Devices.
- /// </summary>
- /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
- /// <param name="userId">Gets or sets the user identifier.</param>
- /// <response code="200">Devices retrieved.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+ /// <summary>
+ /// Get info for a device.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <response code="200">Device info retrieved.</response>
+ /// <response code="404">Device not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ [HttpGet("Info")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
+ {
+ var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+ if (deviceInfo is null)
{
- return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
+ return NotFound();
}
- /// <summary>
- /// Get info for a device.
- /// </summary>
- /// <param name="id">Device Id.</param>
- /// <response code="200">Device info retrieved.</response>
- /// <response code="404">Device not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
- [HttpGet("Info")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
- {
- var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
- if (deviceInfo is null)
- {
- return NotFound();
- }
+ return deviceInfo;
+ }
- return deviceInfo;
+ /// <summary>
+ /// Get options for a device.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <response code="200">Device options retrieved.</response>
+ /// <response code="404">Device not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ [HttpGet("Options")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
+ {
+ var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
+ if (deviceInfo is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get options for a device.
- /// </summary>
- /// <param name="id">Device Id.</param>
- /// <response code="200">Device options retrieved.</response>
- /// <response code="404">Device not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
- [HttpGet("Options")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
- {
- var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
- if (deviceInfo is null)
- {
- return NotFound();
- }
+ return deviceInfo;
+ }
- return deviceInfo;
- }
+ /// <summary>
+ /// Update device options.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <param name="deviceOptions">Device Options.</param>
+ /// <response code="204">Device options updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Options")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> UpdateDeviceOptions(
+ [FromQuery, Required] string id,
+ [FromBody, Required] DeviceOptionsDto deviceOptions)
+ {
+ await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Update device options.
- /// </summary>
- /// <param name="id">Device Id.</param>
- /// <param name="deviceOptions">Device Options.</param>
- /// <response code="204">Device options updated.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Options")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> UpdateDeviceOptions(
- [FromQuery, Required] string id,
- [FromBody, Required] DeviceOptionsDto deviceOptions)
+ /// <summary>
+ /// Deletes a device.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <response code="204">Device deleted.</response>
+ /// <response code="404">Device not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ [HttpDelete]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
+ {
+ var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+ if (existingDevice is null)
{
- await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
- return NoContent();
+ return NotFound();
}
- /// <summary>
- /// Deletes a device.
- /// </summary>
- /// <param name="id">Device Id.</param>
- /// <response code="204">Device deleted.</response>
- /// <response code="404">Device not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
- [HttpDelete]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
- {
- var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
- if (existingDevice is null)
- {
- return NotFound();
- }
-
- var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
-
- foreach (var session in sessions.Items)
- {
- await _sessionManager.Logout(session).ConfigureAwait(false);
- }
+ var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
- return NoContent();
+ foreach (var session in sessions.Items)
+ {
+ await _sessionManager.Logout(session).ConfigureAwait(false);
}
+
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 67cceb4a8..6f0006832 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
@@ -14,201 +13,200 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Display Preferences Controller.
+/// </summary>
+[Authorize]
+public class DisplayPreferencesController : BaseJellyfinApiController
{
+ private readonly IDisplayPreferencesManager _displayPreferencesManager;
+ private readonly ILogger<DisplayPreferencesController> _logger;
+
/// <summary>
- /// Display Preferences Controller.
+ /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class DisplayPreferencesController : BaseJellyfinApiController
+ /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
+ public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
{
- 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>
- /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
- public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
+ _displayPreferencesManager = displayPreferencesManager;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Get Display Preferences.
+ /// </summary>
+ /// <param name="displayPreferencesId">Display preferences id.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="client">Client.</param>
+ /// <response code="200">Display preferences retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
+ [HttpGet("{displayPreferencesId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
+ public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
+ [FromRoute, Required] string displayPreferencesId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery, Required] string client)
+ {
+ if (!Guid.TryParse(displayPreferencesId, out var itemId))
{
- _displayPreferencesManager = displayPreferencesManager;
- _logger = logger;
+ itemId = displayPreferencesId.GetMD5();
}
- /// <summary>
- /// Get Display Preferences.
- /// </summary>
- /// <param name="displayPreferencesId">Display preferences id.</param>
- /// <param name="userId">User id.</param>
- /// <param name="client">Client.</param>
- /// <response code="200">Display preferences retrieved.</response>
- /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
- [HttpGet("{displayPreferencesId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
- public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
- [FromRoute, Required] string displayPreferencesId,
- [FromQuery, Required] Guid userId,
- [FromQuery, Required] string client)
- {
- if (!Guid.TryParse(displayPreferencesId, out var itemId))
- {
- itemId = displayPreferencesId.GetMD5();
- }
+ var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
+ var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
+ itemPreferences.ItemId = itemId;
- var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
- var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
- itemPreferences.ItemId = itemId;
+ var dto = new DisplayPreferencesDto
+ {
+ Client = displayPreferences.Client,
+ Id = displayPreferences.ItemId.ToString(),
+ SortBy = itemPreferences.SortBy,
+ SortOrder = itemPreferences.SortOrder,
+ IndexBy = displayPreferences.IndexBy?.ToString(),
+ RememberIndexing = itemPreferences.RememberIndexing,
+ RememberSorting = itemPreferences.RememberSorting,
+ ScrollDirection = displayPreferences.ScrollDirection,
+ ShowBackdrop = displayPreferences.ShowBackdrop,
+ ShowSidebar = displayPreferences.ShowSidebar
+ };
+
+ foreach (var homeSection in displayPreferences.HomeSections)
+ {
+ dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
+ }
- var dto = new DisplayPreferencesDto
- {
- Client = displayPreferences.Client,
- Id = displayPreferences.ItemId.ToString(),
- SortBy = itemPreferences.SortBy,
- SortOrder = itemPreferences.SortOrder,
- IndexBy = displayPreferences.IndexBy?.ToString(),
- RememberIndexing = itemPreferences.RememberIndexing,
- RememberSorting = itemPreferences.RememberSorting,
- ScrollDirection = displayPreferences.ScrollDirection,
- ShowBackdrop = displayPreferences.ShowBackdrop,
- ShowSidebar = displayPreferences.ShowSidebar
- };
-
- foreach (var homeSection in displayPreferences.HomeSections)
- {
- dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.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;
- 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);
+ foreach (var (key, value) in customDisplayPreferences)
+ {
+ dto.CustomPrefs.TryAdd(key, value);
+ }
- // Load all custom display preferences
- var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
- foreach (var (key, value) in customDisplayPreferences)
- {
- dto.CustomPrefs.TryAdd(key, value);
- }
+ // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
+ _displayPreferencesManager.SaveChanges();
- // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
- _displayPreferencesManager.SaveChanges();
+ return dto;
+ }
- return dto;
+ /// <summary>
+ /// Update Display Preferences.
+ /// </summary>
+ /// <param name="displayPreferencesId">Display preferences id.</param>
+ /// <param name="userId">User Id.</param>
+ /// <param name="client">Client.</param>
+ /// <param name="displayPreferences">New Display Preferences object.</param>
+ /// <response code="204">Display preferences updated.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+ [HttpPost("{displayPreferencesId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
+ public ActionResult UpdateDisplayPreferences(
+ [FromRoute, Required] string displayPreferencesId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery, Required] string client,
+ [FromBody, Required] DisplayPreferencesDto displayPreferences)
+ {
+ HomeSectionType[] defaults =
+ {
+ HomeSectionType.SmallLibraryTiles,
+ HomeSectionType.Resume,
+ HomeSectionType.ResumeAudio,
+ HomeSectionType.ResumeBook,
+ HomeSectionType.LiveTv,
+ HomeSectionType.NextUp,
+ HomeSectionType.LatestMedia,
+ HomeSectionType.None,
+ };
+
+ if (!Guid.TryParse(displayPreferencesId, out var itemId))
+ {
+ itemId = displayPreferencesId.GetMD5();
}
- /// <summary>
- /// Update Display Preferences.
- /// </summary>
- /// <param name="displayPreferencesId">Display preferences id.</param>
- /// <param name="userId">User Id.</param>
- /// <param name="client">Client.</param>
- /// <param name="displayPreferences">New Display Preferences object.</param>
- /// <response code="204">Display preferences updated.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("{displayPreferencesId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
- public ActionResult UpdateDisplayPreferences(
- [FromRoute, Required] string displayPreferencesId,
- [FromQuery, Required] Guid userId,
- [FromQuery, Required] string client,
- [FromBody, Required] DisplayPreferencesDto displayPreferences)
+ var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
+ existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
+ existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
+ existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
+
+ existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
+ existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
+ && !string.IsNullOrEmpty(chromecastVersion)
+ ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
+ : ChromecastVersion.Stable;
+ displayPreferences.CustomPrefs.Remove("chromecastVersion");
+
+ existingDisplayPreferences.EnableNextVideoInfoOverlay = !displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
+ || string.IsNullOrEmpty(enableNextVideoInfoOverlay)
+ || bool.Parse(enableNextVideoInfoOverlay);
+ displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay");
+
+ existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
+ && !string.IsNullOrEmpty(skipBackLength)
+ ? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
+ : 10000;
+ displayPreferences.CustomPrefs.Remove("skipBackLength");
+
+ existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
+ && !string.IsNullOrEmpty(skipForwardLength)
+ ? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
+ : 30000;
+ displayPreferences.CustomPrefs.Remove("skipForwardLength");
+
+ existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
+ ? theme
+ : string.Empty;
+ displayPreferences.CustomPrefs.Remove("dashboardTheme");
+
+ existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
+ ? home
+ : string.Empty;
+ displayPreferences.CustomPrefs.Remove("tvhome");
+
+ existingDisplayPreferences.HomeSections.Clear();
+
+ foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
{
- HomeSectionType[] defaults =
- {
- HomeSectionType.SmallLibraryTiles,
- HomeSectionType.Resume,
- HomeSectionType.ResumeAudio,
- HomeSectionType.ResumeBook,
- HomeSectionType.LiveTv,
- HomeSectionType.NextUp,
- HomeSectionType.LatestMedia,
- HomeSectionType.None,
- };
-
- if (!Guid.TryParse(displayPreferencesId, out var itemId))
+ var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture);
+ if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
{
- itemId = displayPreferencesId.GetMD5();
+ type = order < 8 ? defaults[order] : HomeSectionType.None;
}
- var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
- existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
- existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
- existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
-
- existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
- existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
- && !string.IsNullOrEmpty(chromecastVersion)
- ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
- : ChromecastVersion.Stable;
- displayPreferences.CustomPrefs.Remove("chromecastVersion");
-
- existingDisplayPreferences.EnableNextVideoInfoOverlay = !displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
- || string.IsNullOrEmpty(enableNextVideoInfoOverlay)
- || bool.Parse(enableNextVideoInfoOverlay);
- displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay");
-
- existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
- && !string.IsNullOrEmpty(skipBackLength)
- ? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
- : 10000;
- displayPreferences.CustomPrefs.Remove("skipBackLength");
-
- existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
- && !string.IsNullOrEmpty(skipForwardLength)
- ? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
- : 30000;
- displayPreferences.CustomPrefs.Remove("skipForwardLength");
-
- existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
- ? theme
- : string.Empty;
- displayPreferences.CustomPrefs.Remove("dashboardTheme");
-
- existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
- ? home
- : string.Empty;
- displayPreferences.CustomPrefs.Remove("tvhome");
-
- existingDisplayPreferences.HomeSections.Clear();
-
- foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
- {
- var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture);
- if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
- {
- type = order < 8 ? defaults[order] : HomeSectionType.None;
- }
-
- displayPreferences.CustomPrefs.Remove(key);
- existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
- }
+ displayPreferences.CustomPrefs.Remove(key);
+ existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
+ }
- foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
+ foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
+ {
+ if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
{
- if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
- {
- _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
- displayPreferences.CustomPrefs.Remove(key);
- }
+ _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
+ displayPreferences.CustomPrefs.Remove(key);
}
+ }
- var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client);
- itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName";
- itemPrefs.SortOrder = displayPreferences.SortOrder;
- itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
- itemPrefs.RememberSorting = displayPreferences.RememberSorting;
- itemPrefs.ItemId = itemId;
+ var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client);
+ itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName";
+ itemPrefs.SortOrder = displayPreferences.SortOrder;
+ itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
+ itemPrefs.RememberSorting = displayPreferences.RememberSorting;
+ itemPrefs.ItemId = itemId;
- // Set all remaining custom preferences.
- _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
- _displayPreferencesManager.SaveChanges();
+ // Set all remaining custom preferences.
+ _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
+ _displayPreferencesManager.SaveChanges();
- return NoContent();
- }
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
index 07e0590a1..415385463 100644
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -7,127 +7,126 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Dlna Controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class DlnaController : BaseJellyfinApiController
{
+ private readonly IDlnaManager _dlnaManager;
+
/// <summary>
- /// Dlna Controller.
+ /// Initializes a new instance of the <see cref="DlnaController"/> class.
/// </summary>
- [Authorize(Policy = Policies.RequiresElevation)]
- public class DlnaController : BaseJellyfinApiController
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ public DlnaController(IDlnaManager dlnaManager)
{
- private readonly IDlnaManager _dlnaManager;
+ _dlnaManager = dlnaManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="DlnaController"/> class.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public DlnaController(IDlnaManager dlnaManager)
- {
- _dlnaManager = dlnaManager;
- }
+ /// <summary>
+ /// Get profile infos.
+ /// </summary>
+ /// <response code="200">Device profile infos returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
+ [HttpGet("ProfileInfos")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
+ {
+ return Ok(_dlnaManager.GetProfileInfos());
+ }
- /// <summary>
- /// Get profile infos.
- /// </summary>
- /// <response code="200">Device profile infos returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
- [HttpGet("ProfileInfos")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
- {
- return Ok(_dlnaManager.GetProfileInfos());
- }
+ /// <summary>
+ /// Gets the default profile.
+ /// </summary>
+ /// <response code="200">Default device profile returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
+ [HttpGet("Profiles/Default")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<DeviceProfile> GetDefaultProfile()
+ {
+ return _dlnaManager.GetDefaultProfile();
+ }
- /// <summary>
- /// Gets the default profile.
- /// </summary>
- /// <response code="200">Default device profile returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
- [HttpGet("Profiles/Default")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<DeviceProfile> GetDefaultProfile()
+ /// <summary>
+ /// Gets a single profile.
+ /// </summary>
+ /// <param name="profileId">Profile Id.</param>
+ /// <response code="200">Device profile returned.</response>
+ /// <response code="404">Device profile not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
+ [HttpGet("Profiles/{profileId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
+ {
+ var profile = _dlnaManager.GetProfile(profileId);
+ if (profile is null)
{
- return _dlnaManager.GetDefaultProfile();
+ return NotFound();
}
- /// <summary>
- /// Gets a single profile.
- /// </summary>
- /// <param name="profileId">Profile Id.</param>
- /// <response code="200">Device profile returned.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
- [HttpGet("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
- {
- var profile = _dlnaManager.GetProfile(profileId);
- if (profile is null)
- {
- return NotFound();
- }
+ return profile;
+ }
- return profile;
+ /// <summary>
+ /// Deletes a profile.
+ /// </summary>
+ /// <param name="profileId">Profile id.</param>
+ /// <response code="204">Device profile deleted.</response>
+ /// <response code="404">Device profile not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
+ [HttpDelete("Profiles/{profileId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult DeleteProfile([FromRoute, Required] string profileId)
+ {
+ var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
+ if (existingDeviceProfile is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Deletes a profile.
- /// </summary>
- /// <param name="profileId">Profile id.</param>
- /// <response code="204">Device profile deleted.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
- [HttpDelete("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteProfile([FromRoute, Required] string profileId)
- {
- var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
- if (existingDeviceProfile is null)
- {
- return NotFound();
- }
+ _dlnaManager.DeleteProfile(profileId);
+ return NoContent();
+ }
- _dlnaManager.DeleteProfile(profileId);
- return NoContent();
- }
+ /// <summary>
+ /// Creates a profile.
+ /// </summary>
+ /// <param name="deviceProfile">Device profile.</param>
+ /// <response code="204">Device profile created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Profiles")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
+ {
+ _dlnaManager.CreateProfile(deviceProfile);
+ return NoContent();
+ }
- /// <summary>
- /// Creates a profile.
- /// </summary>
- /// <param name="deviceProfile">Device profile.</param>
- /// <response code="204">Device profile created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Profiles")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
+ /// <summary>
+ /// Updates a profile.
+ /// </summary>
+ /// <param name="profileId">Profile id.</param>
+ /// <param name="deviceProfile">Device profile.</param>
+ /// <response code="204">Device profile updated.</response>
+ /// <response code="404">Device profile not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
+ [HttpPost("Profiles/{profileId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
+ {
+ var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
+ if (existingDeviceProfile is null)
{
- _dlnaManager.CreateProfile(deviceProfile);
- return NoContent();
+ return NotFound();
}
- /// <summary>
- /// Updates a profile.
- /// </summary>
- /// <param name="profileId">Profile id.</param>
- /// <param name="deviceProfile">Device profile.</param>
- /// <response code="204">Device profile updated.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
- [HttpPost("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
- {
- var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
- if (existingDeviceProfile is null)
- {
- return NotFound();
- }
-
- _dlnaManager.UpdateProfile(profileId, deviceProfile);
- return NoContent();
- }
+ _dlnaManager.UpdateProfile(profileId, deviceProfile);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 96c492b3e..95b296fae 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -14,311 +14,310 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Dlna Server Controller.
+/// </summary>
+[Route("Dlna")]
+[DlnaEnabled]
+[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
+public class DlnaServerController : BaseJellyfinApiController
{
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IContentDirectory _contentDirectory;
+ private readonly IConnectionManager _connectionManager;
+ private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
+
/// <summary>
- /// Dlna Server Controller.
+ /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
/// </summary>
- [Route("Dlna")]
- [DlnaEnabled]
- [Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
- public class DlnaServerController : BaseJellyfinApiController
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ public DlnaServerController(IDlnaManager dlnaManager)
{
- private readonly IDlnaManager _dlnaManager;
- private readonly IContentDirectory _contentDirectory;
- private readonly IConnectionManager _connectionManager;
- private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
+ _dlnaManager = dlnaManager;
+ _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
+ _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
+ _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public DlnaServerController(IDlnaManager dlnaManager)
- {
- _dlnaManager = dlnaManager;
- _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
- _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
- _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
- }
+ /// <summary>
+ /// Get Description Xml.
+ /// </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<string> 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);
+ }
- /// <summary>
- /// Get Description Xml.
- /// </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<string> 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);
- }
+ /// <summary>
+ /// Gets Dlna content directory xml.
+ /// </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<string> GetContentDirectory([FromRoute, Required] string serverId)
+ {
+ return Ok(_contentDirectory.GetServiceXml());
+ }
- /// <summary>
- /// Gets Dlna content directory xml.
- /// </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<string> GetContentDirectory([FromRoute, Required] string serverId)
- {
- return Ok(_contentDirectory.GetServiceXml());
- }
+ /// <summary>
+ /// Gets Dlna media receiver registrar xml.
+ /// </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<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
+ {
+ return Ok(_mediaReceiverRegistrar.GetServiceXml());
+ }
- /// <summary>
- /// Gets Dlna media receiver registrar xml.
- /// </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<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
- {
- return Ok(_mediaReceiverRegistrar.GetServiceXml());
- }
+ /// <summary>
+ /// Gets Dlna media receiver registrar xml.
+ /// </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<string> GetConnectionManager([FromRoute, Required] string serverId)
+ {
+ return Ok(_connectionManager.GetServiceXml());
+ }
- /// <summary>
- /// Gets Dlna media receiver registrar xml.
- /// </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<string> GetConnectionManager([FromRoute, Required] string serverId)
- {
- return Ok(_connectionManager.GetServiceXml());
- }
+ /// <summary>
+ /// Process a content directory control request.
+ /// </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);
+ }
- /// <summary>
- /// Process a content directory control request.
- /// </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);
- }
+ /// <summary>
+ /// Process a connection manager control request.
+ /// </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);
+ }
- /// <summary>
- /// Process a connection manager control request.
- /// </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);
- }
+ /// <summary>
+ /// Process a media receiver registrar control request.
+ /// </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);
+ }
- /// <summary>
- /// Process a media receiver registrar control request.
- /// </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);
- }
+ /// <summary>
+ /// Processes an event subscription request.
+ /// </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);
+ }
- /// <summary>
- /// Processes an event subscription request.
- /// </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);
- }
+ /// <summary>
+ /// Processes an event subscription request.
+ /// </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);
+ }
- /// <summary>
- /// Processes an event subscription request.
- /// </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);
- }
+ /// <summary>
+ /// Processes an event subscription request.
+ /// </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);
+ }
- /// <summary>
- /// Processes an event subscription request.
- /// </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);
- }
+ /// <summary>
+ /// Gets a server icon.
+ /// </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);
+ }
- /// <summary>
- /// Gets a server icon.
- /// </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);
- }
+ /// <summary>
+ /// Gets a server icon.
+ /// </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);
+ }
- /// <summary>
- /// Gets a server icon.
- /// </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)
+ private ActionResult GetIconInternal(string fileName)
+ {
+ var icon = _dlnaManager.GetIcon(fileName);
+ if (icon is null)
{
- return GetIconInternal(fileName);
+ return NotFound();
}
- private ActionResult GetIconInternal(string fileName)
- {
- var icon = _dlnaManager.GetIcon(fileName);
- if (icon is null)
- {
- return NotFound();
- }
+ return File(icon.Stream, MimeTypes.GetMimeType(fileName));
+ }
- return File(icon.Stream, MimeTypes.GetMimeType(fileName));
- }
+ private string GetAbsoluteUri()
+ {
+ return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
+ }
- private string GetAbsoluteUri()
+ private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
+ {
+ return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
{
- return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
- }
+ InputXml = requestStream,
+ TargetServerUuId = id,
+ RequestedUrl = GetAbsoluteUri()
+ });
+ }
- private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
+ private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
+ {
+ var subscriptionId = Request.Headers["SID"];
+ if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
{
- return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
- {
- InputXml = requestStream,
- TargetServerUuId = id,
- RequestedUrl = GetAbsoluteUri()
- });
- }
+ var notificationType = Request.Headers["NT"];
+ var callback = Request.Headers["CALLBACK"];
+ var timeoutString = Request.Headers["TIMEOUT"];
- private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
- {
- var subscriptionId = Request.Headers["SID"];
- if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(notificationType))
{
- var notificationType = Request.Headers["NT"];
- var callback = Request.Headers["CALLBACK"];
- var timeoutString = Request.Headers["TIMEOUT"];
-
- if (string.IsNullOrEmpty(notificationType))
- {
- return dlnaEventManager.RenewEventSubscription(
- subscriptionId,
- notificationType,
- timeoutString,
- callback);
- }
-
- return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
+ return dlnaEventManager.RenewEventSubscription(
+ subscriptionId,
+ notificationType,
+ timeoutString,
+ callback);
}
- return dlnaEventManager.CancelEventSubscription(subscriptionId);
+ return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
}
+
+ return dlnaEventManager.CancelEventSubscription(subscriptionId);
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index ba9a57f1d..16c77a923 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -9,7 +9,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
@@ -20,6 +19,8 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@@ -30,2025 +31,2029 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Dynamic hls controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class DynamicHlsController : BaseJellyfinApiController
{
+ private const string DefaultVodEncoderPreset = "veryfast";
+ private const string DefaultEventEncoderPreset = "superfast";
+ private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ILogger<DynamicHlsController> _logger;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator;
+ private readonly DynamicHlsHelper _dynamicHlsHelper;
+ private readonly EncodingOptions _encodingOptions;
+
/// <summary>
- /// Dynamic hls controller.
+ /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class DynamicHlsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <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="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>
+ /// <param name="dynamicHlsPlaylistGenerator">Instance of <see cref="IDynamicHlsPlaylistGenerator"/>.</param>
+ public DynamicHlsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDlnaManager dlnaManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ ILogger<DynamicHlsController> logger,
+ DynamicHlsHelper dynamicHlsHelper,
+ EncodingHelper encodingHelper,
+ IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator)
{
- private const string DefaultVodEncoderPreset = "veryfast";
- private const string DefaultEventEncoderPreset = "superfast";
- private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
-
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly IDeviceManager _deviceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ILogger<DynamicHlsController> _logger;
- private readonly EncodingHelper _encodingHelper;
- private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator;
- private readonly DynamicHlsHelper _dynamicHlsHelper;
- private readonly EncodingOptions _encodingOptions;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <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="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>
- /// <param name="dynamicHlsPlaylistGenerator">Instance of <see cref="IDynamicHlsPlaylistGenerator"/>.</param>
- public DynamicHlsController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDlnaManager dlnaManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- ILogger<DynamicHlsController> logger,
- DynamicHlsHelper dynamicHlsHelper,
- EncodingHelper encodingHelper,
- IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dlnaManager = dlnaManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _logger = logger;
- _dynamicHlsHelper = dynamicHlsHelper;
- _encodingHelper = encodingHelper;
- _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator;
-
- _encodingOptions = serverConfigurationManager.GetEncodingOptions();
- }
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dlnaManager = dlnaManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _logger = logger;
+ _dynamicHlsHelper = dynamicHlsHelper;
+ _encodingHelper = encodingHelper;
+ _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator;
+
+ _encodingOptions = serverConfigurationManager.GetEncodingOptions();
+ }
- /// <summary>
- /// Gets a hls live stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The audio container.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <param name="maxWidth">Optional. The max width.</param>
- /// <param name="maxHeight">Optional. The max height.</param>
- /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
- /// <response code="200">Hls live stream retrieved.</response>
- /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
- [HttpGet("Videos/{itemId}/live.m3u8")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetLiveHlsStream(
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? enableSubtitlesInManifest)
+ /// <summary>
+ /// Gets a hls live stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The audio container.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="maxWidth">Optional. The max width.</param>
+ /// <param name="maxHeight">Optional. The max height.</param>
+ /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
+ /// <response code="200">Hls live stream retrieved.</response>
+ /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
+ [HttpGet("Videos/{itemId}/live.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetLiveHlsStream(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? enableSubtitlesInManifest)
+ {
+ VideoRequestDto streamingRequest = new VideoRequestDto
{
- VideoRequestDto streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions,
- MaxHeight = maxHeight,
- MaxWidth = maxWidth,
- EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
- };
-
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
- // since it gets disposed when ffmpeg exits
- var cancellationToken = cancellationTokenSource.Token;
- var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- TranscodingJobType,
- cancellationToken)
- .ConfigureAwait(false);
-
- TranscodingJobDto? job = null;
- var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
-
- if (!System.IO.File.Exists(playlistPath))
+ Id = itemId,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
+ };
+
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
+ // since it gets disposed when ffmpeg exits
+ var cancellationToken = cancellationTokenSource.Token;
+ var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ TranscodingJobType,
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ TranscodingJobDto? job = null;
+ var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
+
+ if (!System.IO.File.Exists(playlistPath))
+ {
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
{
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ if (!System.IO.File.Exists(playlistPath))
{
- if (!System.IO.File.Exists(playlistPath))
+ // If the playlist doesn't already exist, startup ffmpeg
+ try
{
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- job = await _transcodingJobHelper.StartFfMpeg(
- state,
- playlistPath,
- GetCommandLineArguments(playlistPath, state, true, 0),
- Request,
- TranscodingJobType,
- cancellationTokenSource)
- .ConfigureAwait(false);
- job.IsLiveOutput = true;
- }
- catch
- {
- state.Dispose();
- throw;
- }
+ job = await _transcodingJobHelper.StartFfMpeg(
+ state,
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state, true, 0),
+ Request,
+ TranscodingJobType,
+ cancellationTokenSource)
+ .ConfigureAwait(false);
+ job.IsLiveOutput = true;
+ }
+ catch
+ {
+ state.Dispose();
+ throw;
+ }
- minSegments = state.MinSegments;
- if (minSegments > 0)
- {
- await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
- }
+ minSegments = state.MinSegments;
+ if (minSegments > 0)
+ {
+ await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- transcodingLock.Release();
- }
}
-
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
-
- if (job is not null)
+ finally
{
- _transcodingJobHelper.OnTranscodeEndRequest(job);
+ transcodingLock.Release();
}
-
- var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
-
- return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
}
- /// <summary>
- /// Gets a video hls playlist stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
- /// <response code="200">Video stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
- [HttpGet("Videos/{itemId}/master.m3u8")]
- [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetMasterHlsVideoPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery, Required] string mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
- {
- var streamingRequest = new HlsVideoRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
- };
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ if (job is not null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(job);
}
- /// <summary>
- /// Gets an audio hls playlist stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
- /// <response code="200">Audio stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
- [HttpGet("Audio/{itemId}/master.m3u8")]
- [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetMasterHlsAudioPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery, Required] string mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
- {
- var streamingRequest = new HlsAudioRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate ?? maxStreamingBitrate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
- };
+ var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
- }
+ return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- /// <summary>
- /// Gets a video stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <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("Videos/{itemId}/main.m3u8")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetVariantHlsVideoPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets a video hls playlist stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <response code="200">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
+ [HttpGet("Videos/{itemId}/master.m3u8")]
+ [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetMasterHlsVideoPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery, Required] string mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ {
+ var streamingRequest = new HlsVideoRequestDto
{
- using var cancellationTokenSource = new CancellationTokenSource();
- var streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
-
- return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
- .ConfigureAwait(false);
- }
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions,
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets an audio stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <response code="200">Audio stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("Audio/{itemId}/main.m3u8")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetVariantHlsAudioPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets an audio hls playlist stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
+ [HttpGet("Audio/{itemId}/master.m3u8")]
+ [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetMasterHlsAudioPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery, Required] string mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ {
+ var streamingRequest = new HlsAudioRequestDto
{
- using var cancellationTokenSource = new CancellationTokenSource();
- var streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate ?? maxStreamingBitrate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions,
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ }
- return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
- .ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets a video stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <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("Videos/{itemId}/main.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetVariantHlsVideoPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ using var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new VideoRequestDto
+ {
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
+ .ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a video stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="segmentId">The segment id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
- /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
- /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 desired 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <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("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesVideoFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> GetHlsVideoSegment(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string playlistId,
- [FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
- [FromQuery, Required] long runtimeTicks,
- [FromQuery, Required] long actualSegmentLengthTicks,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets an audio stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("Audio/{itemId}/main.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetVariantHlsAudioPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ using var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new StreamingRequestDto
{
- var streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- CurrentRuntimeTicks = runtimeTicks,
- ActualSegmentLengthTicks = actualSegmentLengthTicks,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
+ .ConfigureAwait(false);
+ }
- return await GetDynamicSegment(streamingRequest, segmentId)
- .ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets a video stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="segmentId">The segment id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
+ /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 desired 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <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("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetHlsVideoSegment(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
+ [FromRoute, Required] string container,
+ [FromQuery, Required] long runtimeTicks,
+ [FromQuery, Required] long actualSegmentLengthTicks,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var streamingRequest = new VideoRequestDto
+ {
+ Id = itemId,
+ CurrentRuntimeTicks = runtimeTicks,
+ ActualSegmentLengthTicks = actualSegmentLengthTicks,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetDynamicSegment(streamingRequest, segmentId)
+ .ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a video stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="segmentId">The segment id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
- /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
- /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <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("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> GetHlsAudioSegment(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string playlistId,
- [FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
- [FromQuery, Required] long runtimeTicks,
- [FromQuery, Required] long actualSegmentLengthTicks,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets a video stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="segmentId">The segment id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
+ /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <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("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetHlsAudioSegment(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
+ [FromRoute, Required] string container,
+ [FromQuery, Required] long runtimeTicks,
+ [FromQuery, Required] long actualSegmentLengthTicks,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var streamingRequest = new StreamingRequestDto
{
- var streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Container = container,
- CurrentRuntimeTicks = runtimeTicks,
- ActualSegmentLengthTicks = actualSegmentLengthTicks,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate ?? maxStreamingBitrate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Container = container,
+ CurrentRuntimeTicks = runtimeTicks,
+ ActualSegmentLengthTicks = actualSegmentLengthTicks,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetDynamicSegment(streamingRequest, segmentId)
+ .ConfigureAwait(false);
+ }
- return await GetDynamicSegment(streamingRequest, segmentId)
- .ConfigureAwait(false);
- }
+ private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource)
+ {
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ TranscodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ var request = new CreateMainPlaylistRequest(
+ state.MediaPath,
+ state.SegmentLength * 1000,
+ state.RunTimeTicks ?? 0,
+ state.Request.SegmentContainer ?? string.Empty,
+ "hls1/main/",
+ Request.QueryString.ToString(),
+ EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
+ var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
+
+ return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource)
+ private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId)
+ {
+ if ((streamingRequest.StartTimeTicks ?? 0) > 0)
{
- using var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- TranscodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- var request = new CreateMainPlaylistRequest(
- state.MediaPath,
- state.SegmentLength * 1000,
- state.RunTimeTicks ?? 0,
- state.Request.SegmentContainer ?? string.Empty,
- "hls1/main/",
- Request.QueryString.ToString(),
- EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
- var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
-
- return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
+ throw new ArgumentException("StartTimeTicks is not allowed.");
}
- private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId)
- {
- if ((streamingRequest.StartTimeTicks ?? 0) > 0)
- {
- throw new ArgumentException("StartTimeTicks is not allowed.");
- }
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ var cancellationToken = cancellationTokenSource.Token;
+
+ var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ TranscodingJobType,
+ cancellationToken)
+ .ConfigureAwait(false);
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- var cancellationToken = cancellationTokenSource.Token;
+ var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
- var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- TranscodingJobType,
- cancellationToken)
- .ConfigureAwait(false);
+ var segmentPath = GetSegmentPath(state, playlistPath, segmentId);
- var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
+ var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
- var segmentPath = GetSegmentPath(state, playlistPath, segmentId);
+ TranscodingJobDto? job;
- var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+ if (System.IO.File.Exists(segmentPath))
+ {
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
+ return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+ }
- TranscodingJobDto? job;
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ var released = false;
+ var startTranscoding = false;
+ try
+ {
if (System.IO.File.Exists(segmentPath))
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
+ transcodingLock.Release();
+ released = true;
+ _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
-
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- var released = false;
- var startTranscoding = false;
-
- try
+ else
{
- if (System.IO.File.Exists(segmentPath))
+ var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+ var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
+
+ if (segmentId == -1)
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- transcodingLock.Release();
- released = true;
- _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+ _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
+ startTranscoding = true;
+ segmentId = 0;
}
- else
+ else if (currentTranscodingIndex is null)
{
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
-
- if (segmentId == -1)
- {
- _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
- startTranscoding = true;
- segmentId = 0;
- }
- else if (currentTranscodingIndex is null)
- {
- _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
- startTranscoding = true;
- }
- else if (segmentId < currentTranscodingIndex.Value)
- {
- _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
- startTranscoding = true;
- }
- else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
- {
- _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
- startTranscoding = true;
- }
+ _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
+ startTranscoding = true;
+ }
+ else if (segmentId < currentTranscodingIndex.Value)
+ {
+ _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
+ startTranscoding = true;
+ }
+ else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
+ {
+ _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
+ startTranscoding = true;
+ }
- if (startTranscoding)
+ if (startTranscoding)
+ {
+ // If the playlist doesn't already exist, startup ffmpeg
+ try
{
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
- .ConfigureAwait(false);
-
- if (currentTranscodingIndex.HasValue)
- {
- DeleteLastFile(playlistPath, segmentExtension, 0);
- }
-
- streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
+ await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
+ .ConfigureAwait(false);
- state.WaitForPath = segmentPath;
- job = await _transcodingJobHelper.StartFfMpeg(
- state,
- playlistPath,
- GetCommandLineArguments(playlistPath, state, false, segmentId),
- Request,
- TranscodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
- }
- catch
+ if (currentTranscodingIndex.HasValue)
{
- state.Dispose();
- throw;
+ DeleteLastFile(playlistPath, segmentExtension, 0);
}
- // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+ streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
+
+ state.WaitForPath = segmentPath;
+ job = await _transcodingJobHelper.StartFfMpeg(
+ state,
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state, false, segmentId),
+ Request,
+ TranscodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
}
- else
+ catch
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- if (job?.TranscodingThrottler is not null)
- {
- await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
- }
+ state.Dispose();
+ throw;
}
+
+ // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
}
- }
- finally
- {
- if (!released)
+ else
{
- transcodingLock.Release();
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ if (job?.TranscodingThrottler is not null)
+ {
+ await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
+ }
}
}
-
- _logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
-
- private static double[] GetSegmentLengths(StreamState state)
- => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
-
- internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength)
+ finally
{
- var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks;
- var wholeSegments = runtimeTicks / segmentLengthTicks;
- var remainingTicks = runtimeTicks % segmentLengthTicks;
-
- var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
- var segments = new double[segmentsLen];
- for (int i = 0; i < wholeSegments; i++)
+ if (!released)
{
- segments[i] = segmentlength;
+ transcodingLock.Release();
}
+ }
- if (remainingTicks != 0)
- {
- segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
- }
+ _logger.LogDebug("returning {0} [general case]", segmentPath);
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+ }
+
+ private static double[] GetSegmentLengths(StreamState state)
+ => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
- return segments;
+ internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength)
+ {
+ var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks;
+ var wholeSegments = runtimeTicks / segmentLengthTicks;
+ var remainingTicks = runtimeTicks % segmentLengthTicks;
+
+ var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
+ var segments = new double[segmentsLen];
+ for (int i = 0; i < wholeSegments; i++)
+ {
+ segments[i] = segmentlength;
}
- private string GetCommandLineArguments(string outputPath, StreamState state, bool isEventPlaylist, int startNumber)
+ if (remainingTicks != 0)
{
- var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
+ segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
+ }
- if (state.BaseRequest.BreakOnNonKeyFrames)
- {
- // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
- // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
- // to produce a missing part of video stream before first keyframe is encountered, which may lead to
- // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
- _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
- state.BaseRequest.BreakOnNonKeyFrames = false;
- }
+ return segments;
+ }
- var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
+ private string GetCommandLineArguments(string outputPath, StreamState state, bool isEventPlaylist, int startNumber)
+ {
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
- var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
- var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
- var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
- var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
- var outputTsArg = outputPrefix + "%d" + outputExtension;
+ if (state.BaseRequest.BreakOnNonKeyFrames)
+ {
+ // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
+ // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
+ // to produce a missing part of video stream before first keyframe is encountered, which may lead to
+ // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
+ _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
+ state.BaseRequest.BreakOnNonKeyFrames = false;
+ }
- var segmentFormat = string.Empty;
- var segmentContainer = outputExtension.TrimStart('.');
- var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
+ var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
- if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
- {
- segmentFormat = "mpegts";
- }
- else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch
- {
- // on Windows, the path of fmp4 header file needs to be configured
- true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
- // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
- false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
- };
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+ var outputTsArg = outputPrefix + "%d" + outputExtension;
- segmentFormat = "fmp4" + outputFmp4HeaderArg;
- }
- else
+ var segmentFormat = string.Empty;
+ var segmentContainer = outputExtension.TrimStart('.');
+ var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
+
+ if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ segmentFormat = "mpegts";
+ }
+ else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch
{
- _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer);
- segmentFormat = "mpegts";
- }
+ // on Windows, the path of fmp4 header file needs to be configured
+ true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
+ // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
+ false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
+ };
- var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
- ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
- : "128";
+ segmentFormat = "fmp4" + outputFmp4HeaderArg;
+ }
+ else
+ {
+ _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer);
+ segmentFormat = "mpegts";
+ }
- var baseUrlParam = string.Empty;
- if (isEventPlaylist)
- {
- baseUrlParam = string.Format(
- CultureInfo.InvariantCulture,
- " -hls_base_url \"hls/{0}/\"",
- Path.GetFileNameWithoutExtension(outputPath));
- }
+ var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
+ ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+ : "128";
- return string.Format(
+ var baseUrlParam = string.Empty;
+ if (isEventPlaylist)
+ {
+ baseUrlParam = 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 \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"",
- inputModifier,
- _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
- threads,
- mapArgs,
- GetVideoArguments(state, startNumber, isEventPlaylist),
- GetAudioArguments(state),
- maxMuxingQueueSize,
- state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- segmentFormat,
- startNumber.ToString(CultureInfo.InvariantCulture),
- baseUrlParam,
- isEventPlaylist ? "event" : "vod",
- outputTsArg,
- outputPath).Trim();
+ " -hls_base_url \"hls/{0}/\"",
+ Path.GetFileNameWithoutExtension(outputPath));
+ }
+
+ 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 \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"",
+ inputModifier,
+ _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
+ threads,
+ mapArgs,
+ GetVideoArguments(state, startNumber, isEventPlaylist),
+ GetAudioArguments(state),
+ maxMuxingQueueSize,
+ state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+ segmentFormat,
+ startNumber.ToString(CultureInfo.InvariantCulture),
+ baseUrlParam,
+ isEventPlaylist ? "event" : "vod",
+ EncodingUtils.NormalizePath(outputTsArg),
+ EncodingUtils.NormalizePath(outputPath)).Trim();
+ }
+
+ /// <summary>
+ /// Gets the audio arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The command line arguments for audio transcoding.</returns>
+ private string GetAudioArguments(StreamState state)
+ {
+ if (state.AudioStream is null)
+ {
+ return string.Empty;
}
- /// <summary>
- /// Gets the audio arguments for transcoding.
- /// </summary>
- /// <param name="state">The <see cref="StreamState"/>.</param>
- /// <returns>The command line arguments for audio transcoding.</returns>
- private string GetAudioArguments(StreamState state)
+ var audioCodec = _encodingHelper.GetAudioEncoder(state);
+
+ if (!state.IsOutputVideo)
{
- if (state.AudioStream is null)
+ if (EncodingHelper.IsCopyCodec(audioCodec))
{
- return string.Empty;
- }
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
- var audioCodec = _encodingHelper.GetAudioEncoder(state);
+ return "-acodec copy -strict -2" + bitStreamArgs;
+ }
- if (!state.IsOutputVideo)
- {
- if (EncodingHelper.IsCopyCodec(audioCodec))
- {
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+ var audioTranscodeParams = string.Empty;
- return "-acodec copy -strict -2" + bitStreamArgs;
- }
+ audioTranscodeParams += "-acodec " + audioCodec;
- var audioTranscodeParams = string.Empty;
+ if (state.OutputAudioBitrate.HasValue)
+ {
+ audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
- audioTranscodeParams += "-acodec " + audioCodec;
+ if (state.OutputAudioChannels.HasValue)
+ {
+ audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
+ }
- if (state.OutputAudioBitrate.HasValue)
- {
- audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
- if (state.OutputAudioChannels.HasValue)
- {
- audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
- }
+ audioTranscodeParams += " -vn";
+ return audioTranscodeParams;
+ }
- if (state.OutputAudioSampleRate.HasValue)
- {
- audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ // dts, flac, opus and truehd are experimental in mp4 muxer
+ var strictArgs = string.Empty;
- audioTranscodeParams += " -vn";
- return audioTranscodeParams;
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
+ {
+ strictArgs = " -strict -2";
+ }
- // dts, flac and opus are experimental in mp4 muxer
- var strictArgs = string.Empty;
+ if (EncodingHelper.IsCopyCodec(audioCodec))
+ {
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+ var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
- if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
+ if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
- strictArgs = " -strict -2";
+ return copyArgs + " -copypriorss:a:0 0";
}
- if (EncodingHelper.IsCopyCodec(audioCodec))
- {
- var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
- var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
+ return copyArgs;
+ }
- if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
- {
- return copyArgs + " -copypriorss:a:0 0";
- }
+ var args = "-codec:a:0 " + audioCodec + strictArgs;
- return copyArgs;
- }
+ var channels = state.OutputAudioChannels;
- var args = "-codec:a:0 " + audioCodec + strictArgs;
+ if (channels.HasValue
+ && (channels.Value != 2
+ || (state.AudioStream is not null
+ && state.AudioStream.Channels.HasValue
+ && state.AudioStream.Channels.Value > 5
+ && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
+ {
+ args += " -ac " + channels.Value;
+ }
- var channels = state.OutputAudioChannels;
+ var bitrate = state.OutputAudioBitrate;
- if (channels.HasValue
- && (channels.Value != 2
- || (state.AudioStream is not null
- && state.AudioStream.Channels.HasValue
- && state.AudioStream.Channels.Value > 5
- && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
- {
- args += " -ac " + channels.Value;
- }
+ if (bitrate.HasValue)
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
- var bitrate = state.OutputAudioBitrate;
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
- if (bitrate.HasValue)
- {
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
- if (state.OutputAudioSampleRate.HasValue)
- {
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ return args;
+ }
- args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
+ /// <summary>
+ /// Gets the video arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <param name="startNumber">The first number in the hls sequence.</param>
+ /// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
+ /// <returns>The command line arguments for video transcoding.</returns>
+ private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
+ {
+ if (state.VideoStream is null)
+ {
+ return string.Empty;
+ }
- return args;
+ if (!state.IsOutputVideo)
+ {
+ return string.Empty;
}
- /// <summary>
- /// Gets the video arguments for transcoding.
- /// </summary>
- /// <param name="state">The <see cref="StreamState"/>.</param>
- /// <param name="startNumber">The first number in the hls sequence.</param>
- /// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
- /// <returns>The command line arguments for video transcoding.</returns>
- private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
+ var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+
+ var args = "-codec:v:0 " + codec;
+
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- if (state.VideoStream is null)
+ if (EncodingHelper.IsCopyCodec(codec)
+ && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
{
- return string.Empty;
+ // Prefer dvh1 to dvhe
+ args += " -tag:v:0 dvh1 -strict -2";
}
-
- if (!state.IsOutputVideo)
+ else
{
- return string.Empty;
+ // Prefer hvc1 to hev1
+ args += " -tag:v:0 hvc1";
}
+ }
- var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
-
- var args = "-codec:v:0 " + codec;
+ // if (state.EnableMpegtsM2TsMode)
+ // {
+ // args += " -mpegts_m2ts_mode 1";
+ // }
- if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ // See if we can save come cpu cycles by avoiding encoding.
+ if (EncodingHelper.IsCopyCodec(codec))
+ {
+ // 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))
{
- if (EncodingHelper.IsCopyCodec(codec)
- && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
+ if (!string.IsNullOrEmpty(bitStreamArgs))
{
- // Prefer dvh1 to dvhe
- args += " -tag:v:0 dvh1 -strict -2";
- }
- else
- {
- // Prefer hvc1 to hev1
- args += " -tag:v:0 hvc1";
+ args += " " + bitStreamArgs;
}
}
- // if (state.EnableMpegtsM2TsMode)
- // {
- // args += " -mpegts_m2ts_mode 1";
- // }
+ args += " -start_at_zero";
+ }
+ else
+ {
+ args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, isEventPlaylist ? DefaultEventEncoderPreset : DefaultVodEncoderPreset);
- // See if we can save come cpu cycles by avoiding encoding.
- if (EncodingHelper.IsCopyCodec(codec))
- {
- // 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);
- if (!string.IsNullOrEmpty(bitStreamArgs))
- {
- args += " " + bitStreamArgs;
- }
- }
+ // Set the key frame params for video encoding to match the hls segment time.
+ args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
- args += " -start_at_zero";
- }
- else
+ // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
- args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, isEventPlaylist ? DefaultEventEncoderPreset : DefaultVodEncoderPreset);
+ args += " -bf 0";
+ }
- // Set the key frame params for video encoding to match the hls segment time.
- args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
+ // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
- // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
- if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
- {
- args += " -bf 0";
- }
+ // video processing filters.
+ var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
- // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
+ var negativeMapArgs = _encodingHelper.GetNegativeMapArgsByFilters(state, videoProcessParam);
- // video processing filters.
- args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
+ args = negativeMapArgs + args + videoProcessParam;
- // -start_at_zero is necessary to use with -ss when seeking,
- // otherwise the target position cannot be determined.
- if (state.SubtitleStream is not null)
+ // -start_at_zero is necessary to use with -ss when seeking,
+ // otherwise the target position cannot be determined.
+ if (state.SubtitleStream is not null)
+ {
+ // Disable start_at_zero for external graphical subs
+ if (!(state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
{
- // Disable start_at_zero for external graphical subs
- if (!(state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
- {
- args += " -start_at_zero";
- }
+ args += " -start_at_zero";
}
}
+ }
- // TODO why was this not enabled for VOD?
- if (isEventPlaylist)
- {
- args += " -flags -global_header";
- }
+ // TODO why was this not enabled for VOD?
+ if (isEventPlaylist)
+ {
+ args += " -flags -global_header";
+ }
- if (!string.IsNullOrEmpty(state.OutputVideoSync))
- {
- args += " -vsync " + state.OutputVideoSync;
- }
+ if (!string.IsNullOrEmpty(state.OutputVideoSync))
+ {
+ args += " -vsync " + state.OutputVideoSync;
+ }
- args += _encodingHelper.GetOutputFFlags(state);
+ args += _encodingHelper.GetOutputFFlags(state);
- return args;
- }
+ return args;
+ }
- private string GetSegmentPath(StreamState state, string playlist, int index)
- {
- var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
- var filename = Path.GetFileNameWithoutExtension(playlist);
+ private string GetSegmentPath(StreamState state, string playlist, int index)
+ {
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
+ var filename = Path.GetFileNameWithoutExtension(playlist);
- return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer));
- }
+ return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer));
+ }
- private async Task<ActionResult> GetSegmentResult(
- StreamState state,
- string playlistPath,
- string segmentPath,
- string segmentExtension,
- int segmentIndex,
- TranscodingJobDto? transcodingJob,
- CancellationToken cancellationToken)
+ private async Task<ActionResult> GetSegmentResult(
+ StreamState state,
+ string playlistPath,
+ string segmentPath,
+ string segmentExtension,
+ int segmentIndex,
+ TranscodingJobDto? transcodingJob,
+ CancellationToken cancellationToken)
+ {
+ var segmentExists = System.IO.File.Exists(segmentPath);
+ if (segmentExists)
{
- var segmentExists = System.IO.File.Exists(segmentPath);
- if (segmentExists)
+ if (transcodingJob is not null && transcodingJob.HasExited)
{
- if (transcodingJob is not null && transcodingJob.HasExited)
- {
- // Transcoding job is over, so assume all existing files are ready
- _logger.LogDebug("serving up {0} as transcode is over", segmentPath);
- return GetSegmentResult(state, segmentPath, transcodingJob);
- }
+ // Transcoding job is over, so assume all existing files are ready
+ _logger.LogDebug("serving up {0} as transcode is over", segmentPath);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
+ }
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+ var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
- if (segmentIndex < currentTranscodingIndex)
- {
- _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
- return GetSegmentResult(state, segmentPath, transcodingJob);
- }
+ // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
+ if (segmentIndex < currentTranscodingIndex)
+ {
+ _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
+ }
- var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
- if (transcodingJob is not null)
+ var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
+ if (transcodingJob is not null)
+ {
+ while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
{
- while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
+ // To be considered ready, the segment file has to exist AND
+ // either the transcoding job should be done or next segment should also exist
+ if (segmentExists)
{
- // To be considered ready, the segment file has to exist AND
- // either the transcoding job should be done or next segment should also exist
- if (segmentExists)
- {
- if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath))
- {
- _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath);
- return GetSegmentResult(state, segmentPath, transcodingJob);
- }
- }
- else
+ if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath))
{
- segmentExists = System.IO.File.Exists(segmentPath);
- if (segmentExists)
- {
- continue; // avoid unnecessary waiting if segment just became available
- }
+ _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
-
- if (!System.IO.File.Exists(segmentPath))
- {
- _logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
}
else
{
- _logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
+ segmentExists = System.IO.File.Exists(segmentPath);
+ if (segmentExists)
+ {
+ continue; // avoid unnecessary waiting if segment just became available
+ }
}
- cancellationToken.ThrowIfCancellationRequested();
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (!System.IO.File.Exists(segmentPath))
+ {
+ _logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
}
else
{
- _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
+ _logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
}
- return GetSegmentResult(state, segmentPath, transcodingJob);
+ cancellationToken.ThrowIfCancellationRequested();
}
-
- private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob)
+ else
{
- var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks;
-
- Response.OnCompleted(() =>
- {
- _logger.LogDebug("Finished serving {SegmentPath}", segmentPath);
- if (transcodingJob is not null)
- {
- transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
- _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
- }
+ _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
+ }
- return Task.CompletedTask;
- });
+ return GetSegmentResult(state, segmentPath, transcodingJob);
+ }
- return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath));
- }
+ private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob)
+ {
+ var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks;
- private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
+ Response.OnCompleted(() =>
{
- var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
-
- if (job is null || job.HasExited)
+ _logger.LogDebug("Finished serving {SegmentPath}", segmentPath);
+ if (transcodingJob is not null)
{
- return null;
+ transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
+ _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
}
- var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem);
+ return Task.CompletedTask;
+ });
- if (file is null)
- {
- return null;
- }
-
- var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath));
+ }
- var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+ private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
+ {
+ var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
- return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ if (job is null || job.HasExited)
+ {
+ return null;
}
- private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
+ var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem);
+
+ if (file is null)
{
- var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
+ return null;
+ }
- var filePrefix = Path.GetFileNameWithoutExtension(playlist);
+ var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
- try
- {
- return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
- .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
- .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
- .FirstOrDefault();
- }
- catch (IOException)
- {
- return null;
- }
- }
+ var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+
+ return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ }
+
+ private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
+ {
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
- private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ var filePrefix = Path.GetFileNameWithoutExtension(playlist);
+
+ try
+ {
+ return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
+ .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
+ .FirstOrDefault();
+ }
+ catch (IOException)
{
- var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
+ return null;
+ }
+ }
- if (file is not null)
- {
- DeleteFile(file.FullName, retryCount);
- }
+ private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ {
+ var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
+
+ if (file is not null)
+ {
+ DeleteFile(file.FullName, retryCount);
}
+ }
- private void DeleteFile(string path, int retryCount)
+ private void DeleteFile(string path, int retryCount)
+ {
+ if (retryCount >= 5)
{
- if (retryCount >= 5)
- {
- return;
- }
+ return;
+ }
- _logger.LogDebug("Deleting partial HLS file {Path}", path);
+ _logger.LogDebug("Deleting partial HLS file {Path}", path);
- try
- {
- _fileSystem.DeleteFile(path);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+ try
+ {
+ _fileSystem.DeleteFile(path);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- var task = Task.Delay(100);
- task.Wait();
- DeleteFile(path, retryCount + 1);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- }
+ var task = Task.Delay(100);
+ task.Wait();
+ DeleteFile(path, retryCount + 1);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
}
}
}
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 6c78a7987..8c9ee1a19 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -12,186 +12,185 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Environment Controller.
+/// </summary>
+[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+public class EnvironmentController : BaseJellyfinApiController
{
+ private const char UncSeparator = '\\';
+ private const string UncStartPrefix = @"\\";
+
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<EnvironmentController> _logger;
+
/// <summary>
- /// Environment Controller.
+ /// Initializes a new instance of the <see cref="EnvironmentController"/> class.
/// </summary>
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- public class EnvironmentController : BaseJellyfinApiController
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
+ public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
{
- private const char UncSeparator = '\\';
- private const string UncStartPrefix = @"\\";
-
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<EnvironmentController> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EnvironmentController"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
- public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
- {
- _fileSystem = fileSystem;
- _logger = logger;
- }
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
- /// <summary>
- /// Gets the contents of a given directory in the file system.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
- /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
- /// <response code="200">Directory contents returned.</response>
- /// <returns>Directory contents.</returns>
- [HttpGet("DirectoryContents")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
- [FromQuery, Required] string path,
- [FromQuery] bool includeFiles = false,
- [FromQuery] bool includeDirectories = false)
+ /// <summary>
+ /// Gets the contents of a given directory in the file system.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
+ /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
+ /// <response code="200">Directory contents returned.</response>
+ /// <returns>Directory contents.</returns>
+ [HttpGet("DirectoryContents")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
+ [FromQuery, Required] string path,
+ [FromQuery] bool includeFiles = false,
+ [FromQuery] bool includeDirectories = false)
+ {
+ if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
+ && path.LastIndexOf(UncSeparator) == 1)
{
- if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
- && path.LastIndexOf(UncSeparator) == 1)
- {
- return Array.Empty<FileSystemEntryInfo>();
- }
+ return Array.Empty<FileSystemEntryInfo>();
+ }
- var entries =
- _fileSystem.GetFileSystemEntries(path)
- .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
- .OrderBy(i => i.FullName);
+ var entries =
+ _fileSystem.GetFileSystemEntries(path)
+ .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
+ .OrderBy(i => i.FullName);
- return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
- }
+ return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
+ }
- /// <summary>
- /// Validates path.
- /// </summary>
- /// <param name="validatePathDto">Validate request object.</param>
- /// <response code="204">Path validated.</response>
- /// <response code="404">Path not found.</response>
- /// <returns>Validation status.</returns>
- [HttpPost("ValidatePath")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
+ /// <summary>
+ /// Validates path.
+ /// </summary>
+ /// <param name="validatePathDto">Validate request object.</param>
+ /// <response code="204">Path validated.</response>
+ /// <response code="404">Path not found.</response>
+ /// <returns>Validation status.</returns>
+ [HttpPost("ValidatePath")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
+ {
+ if (validatePathDto.IsFile.HasValue)
{
- if (validatePathDto.IsFile.HasValue)
+ if (validatePathDto.IsFile.Value)
{
- if (validatePathDto.IsFile.Value)
+ if (!System.IO.File.Exists(validatePathDto.Path))
{
- if (!System.IO.File.Exists(validatePathDto.Path))
- {
- return NotFound();
- }
- }
- else
- {
- if (!Directory.Exists(validatePathDto.Path))
- {
- return NotFound();
- }
+ return NotFound();
}
}
else
{
- if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
+ if (!Directory.Exists(validatePathDto.Path))
{
return NotFound();
}
+ }
+ }
+ else
+ {
+ if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
+ {
+ return NotFound();
+ }
- if (validatePathDto.ValidateWritable)
+ if (validatePathDto.ValidateWritable)
+ {
+ if (validatePathDto.Path is null)
{
- if (validatePathDto.Path is null)
- {
- throw new ResourceNotFoundException(nameof(validatePathDto.Path));
- }
+ throw new ResourceNotFoundException(nameof(validatePathDto.Path));
+ }
- var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
- try
- {
- System.IO.File.WriteAllText(file, string.Empty);
- }
- finally
+ var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
+ try
+ {
+ System.IO.File.WriteAllText(file, string.Empty);
+ }
+ finally
+ {
+ if (System.IO.File.Exists(file))
{
- if (System.IO.File.Exists(file))
- {
- System.IO.File.Delete(file);
- }
+ System.IO.File.Delete(file);
}
}
}
-
- return NoContent();
}
- /// <summary>
- /// Gets network paths.
- /// </summary>
- /// <response code="200">Empty array returned.</response>
- /// <returns>List of entries.</returns>
- [Obsolete("This endpoint is obsolete.")]
- [HttpGet("NetworkShares")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
- {
- _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
- return Array.Empty<FileSystemEntryInfo>();
- }
+ return NoContent();
+ }
- /// <summary>
- /// Gets available drives from the server's file system.
- /// </summary>
- /// <response code="200">List of entries returned.</response>
- /// <returns>List of entries.</returns>
- [HttpGet("Drives")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<FileSystemEntryInfo> GetDrives()
- {
- return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
- }
+ /// <summary>
+ /// Gets network paths.
+ /// </summary>
+ /// <response code="200">Empty array returned.</response>
+ /// <returns>List of entries.</returns>
+ [Obsolete("This endpoint is obsolete.")]
+ [HttpGet("NetworkShares")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
+ {
+ _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
+ return Array.Empty<FileSystemEntryInfo>();
+ }
- /// <summary>
- /// Gets the parent path of a given path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>Parent path.</returns>
- [HttpGet("ParentPath")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<string?> GetParentPath([FromQuery, Required] string path)
+ /// <summary>
+ /// Gets available drives from the server's file system.
+ /// </summary>
+ /// <response code="200">List of entries returned.</response>
+ /// <returns>List of entries.</returns>
+ [HttpGet("Drives")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<FileSystemEntryInfo> GetDrives()
+ {
+ return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
+ }
+
+ /// <summary>
+ /// Gets the parent path of a given path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>Parent path.</returns>
+ [HttpGet("ParentPath")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<string?> GetParentPath([FromQuery, Required] string path)
+ {
+ string? parent = Path.GetDirectoryName(path);
+ if (string.IsNullOrEmpty(parent))
{
- string? parent = Path.GetDirectoryName(path);
- if (string.IsNullOrEmpty(parent))
+ // Check if unc share
+ var index = path.LastIndexOf(UncSeparator);
+
+ if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
{
- // Check if unc share
- var index = path.LastIndexOf(UncSeparator);
+ parent = path.Substring(0, index);
- if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
+ if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
{
- parent = path.Substring(0, index);
-
- if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
- {
- parent = null;
- }
+ parent = null;
}
}
-
- return parent;
}
- /// <summary>
- /// Get Default directory browser.
- /// </summary>
- /// <response code="200">Default directory browser returned.</response>
- /// <returns>Default directory browser.</returns>
- [HttpGet("DefaultDirectoryBrowser")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
- {
- return new DefaultDirectoryBrowserInfoDto();
- }
+ return parent;
+ }
+
+ /// <summary>
+ /// Get Default directory browser.
+ /// </summary>
+ /// <response code="200">Default directory browser returned.</response>
+ /// <returns>Default directory browser.</returns>
+ [HttpGet("DefaultDirectoryBrowser")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
+ {
+ return new DefaultDirectoryBrowserInfoDto();
}
}
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 17d136384..dac07429f 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,6 +1,7 @@
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;
@@ -12,205 +13,206 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Filters controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class FilterController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// Filters controller.
+ /// Initializes a new instance of the <see cref="FilterController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class FilterController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public FilterController(ILibraryManager libraryManager, IUserManager userManager)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FilterController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public FilterController(ILibraryManager libraryManager, IUserManager userManager)
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ }
+
+ /// <summary>
+ /// Gets legacy query filters.
+ /// </summary>
+ /// <param name="userId">Optional. User id.</param>
+ /// <param name="parentId">Optional. Parent id.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <response code="200">Legacy filters retrieved.</response>
+ /// <returns>Legacy query filters.</returns>
+ [HttpGet("Items/Filters")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
+ [FromQuery] Guid? userId,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ BaseItem? item = null;
+ if (includeItemTypes.Length != 1
+ || !(includeItemTypes[0] == BaseItemKind.BoxSet
+ || includeItemTypes[0] == BaseItemKind.Playlist
+ || includeItemTypes[0] == BaseItemKind.Trailer
+ || includeItemTypes[0] == BaseItemKind.Program))
{
- _libraryManager = libraryManager;
- _userManager = userManager;
+ item = _libraryManager.GetParentItem(parentId, user?.Id);
}
- /// <summary>
- /// Gets legacy query filters.
- /// </summary>
- /// <param name="userId">Optional. User id.</param>
- /// <param name="parentId">Optional. Parent id.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
- /// <response code="200">Legacy filters retrieved.</response>
- /// <returns>Legacy query filters.</returns>
- [HttpGet("Items/Filters")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
- [FromQuery] Guid? userId,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
+ var query = new InternalItemsQuery
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- BaseItem? item = null;
- if (includeItemTypes.Length != 1
- || !(includeItemTypes[0] == BaseItemKind.BoxSet
- || includeItemTypes[0] == BaseItemKind.Playlist
- || includeItemTypes[0] == BaseItemKind.Trailer
- || includeItemTypes[0] == BaseItemKind.Program))
- {
- item = _libraryManager.GetParentItem(parentId, user?.Id);
- }
-
- var query = new InternalItemsQuery
- {
- User = user,
- MediaTypes = mediaTypes,
- IncludeItemTypes = includeItemTypes,
- Recursive = true,
- EnableTotalRecordCount = false,
- DtoOptions = new DtoOptions
- {
- Fields = new[] { ItemFields.Genres, ItemFields.Tags },
- EnableImages = false,
- EnableUserData = false
- }
- };
-
- if (item is not Folder folder)
+ User = user,
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
+ Recursive = true,
+ EnableTotalRecordCount = false,
+ DtoOptions = new DtoOptions
{
- return new QueryFiltersLegacy();
+ Fields = new[] { ItemFields.Genres, ItemFields.Tags },
+ EnableImages = false,
+ EnableUserData = false
}
+ };
- var itemList = folder.GetItemList(query);
- return new QueryFiltersLegacy
- {
- Years = itemList.Select(i => i.ProductionYear ?? -1)
- .Where(i => i > 0)
- .Distinct()
- .Order()
- .ToArray(),
-
- Genres = itemList.SelectMany(i => i.Genres)
- .DistinctNames()
- .Order()
- .ToArray(),
-
- Tags = itemList
- .SelectMany(i => i.Tags)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .Order()
- .ToArray(),
-
- OfficialRatings = itemList
- .Select(i => i.OfficialRating)
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .Order()
- .ToArray()
- };
+ if (item is not Folder folder)
+ {
+ return new QueryFiltersLegacy();
}
- /// <summary>
- /// Gets query filters.
- /// </summary>
- /// <param name="userId">Optional. User id.</param>
- /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isAiring">Optional. Is item airing.</param>
- /// <param name="isMovie">Optional. Is item movie.</param>
- /// <param name="isSports">Optional. Is item sports.</param>
- /// <param name="isKids">Optional. Is item kids.</param>
- /// <param name="isNews">Optional. Is item news.</param>
- /// <param name="isSeries">Optional. Is item series.</param>
- /// <param name="recursive">Optional. Search recursive.</param>
- /// <response code="200">Filters retrieved.</response>
- /// <returns>Query filters.</returns>
- [HttpGet("Items/Filters2")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryFilters> GetQueryFilters(
- [FromQuery] Guid? userId,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isAiring,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSports,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? recursive)
+ var itemList = folder.GetItemList(query);
+ return new QueryFiltersLegacy
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- BaseItem? parentItem = null;
- if (includeItemTypes.Length == 1
- && (includeItemTypes[0] == BaseItemKind.BoxSet
- || includeItemTypes[0] == BaseItemKind.Playlist
- || includeItemTypes[0] == BaseItemKind.Trailer
- || includeItemTypes[0] == BaseItemKind.Program))
- {
- parentItem = null;
- }
- else if (parentId.HasValue)
- {
- parentItem = _libraryManager.GetItemById(parentId.Value);
- }
+ Years = itemList.Select(i => i.ProductionYear ?? -1)
+ .Where(i => i > 0)
+ .Distinct()
+ .Order()
+ .ToArray(),
+
+ Genres = itemList.SelectMany(i => i.Genres)
+ .DistinctNames()
+ .Order()
+ .ToArray(),
+
+ Tags = itemList
+ .SelectMany(i => i.Tags)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Order()
+ .ToArray(),
+
+ OfficialRatings = itemList
+ .Select(i => i.OfficialRating)
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Order()
+ .ToArray()
+ };
+ }
- var filters = new QueryFilters();
- var genreQuery = new InternalItemsQuery(user)
- {
- IncludeItemTypes = includeItemTypes,
- DtoOptions = new DtoOptions
- {
- Fields = Array.Empty<ItemFields>(),
- EnableImages = false,
- EnableUserData = false
- },
- IsAiring = isAiring,
- IsMovie = isMovie,
- IsSports = isSports,
- IsKids = isKids,
- IsNews = isNews,
- IsSeries = isSeries
- };
-
- if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
- {
- genreQuery.AncestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
- }
- else
+ /// <summary>
+ /// Gets query filters.
+ /// </summary>
+ /// <param name="userId">Optional. User id.</param>
+ /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isAiring">Optional. Is item airing.</param>
+ /// <param name="isMovie">Optional. Is item movie.</param>
+ /// <param name="isSports">Optional. Is item sports.</param>
+ /// <param name="isKids">Optional. Is item kids.</param>
+ /// <param name="isNews">Optional. Is item news.</param>
+ /// <param name="isSeries">Optional. Is item series.</param>
+ /// <param name="recursive">Optional. Search recursive.</param>
+ /// <response code="200">Filters retrieved.</response>
+ /// <returns>Query filters.</returns>
+ [HttpGet("Items/Filters2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryFilters> GetQueryFilters(
+ [FromQuery] Guid? userId,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isAiring,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? recursive)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ BaseItem? parentItem = null;
+ if (includeItemTypes.Length == 1
+ && (includeItemTypes[0] == BaseItemKind.BoxSet
+ || includeItemTypes[0] == BaseItemKind.Playlist
+ || includeItemTypes[0] == BaseItemKind.Trailer
+ || includeItemTypes[0] == BaseItemKind.Program))
+ {
+ parentItem = null;
+ }
+ else if (parentId.HasValue)
+ {
+ parentItem = _libraryManager.GetItemById(parentId.Value);
+ }
+
+ var filters = new QueryFilters();
+ var genreQuery = new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = includeItemTypes,
+ DtoOptions = new DtoOptions
{
- genreQuery.Parent = parentItem;
- }
+ Fields = Array.Empty<ItemFields>(),
+ EnableImages = false,
+ EnableUserData = false
+ },
+ IsAiring = isAiring,
+ IsMovie = isMovie,
+ IsSports = isSports,
+ IsKids = isKids,
+ IsNews = isNews,
+ IsSeries = isSeries
+ };
+
+ if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
+ {
+ genreQuery.AncestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
+ }
+ else
+ {
+ genreQuery.Parent = parentItem;
+ }
- if (includeItemTypes.Length == 1
- && (includeItemTypes[0] == BaseItemKind.MusicAlbum
- || includeItemTypes[0] == BaseItemKind.MusicVideo
- || includeItemTypes[0] == BaseItemKind.MusicArtist
- || includeItemTypes[0] == BaseItemKind.Audio))
+ if (includeItemTypes.Length == 1
+ && (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
{
- filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
- {
- Name = i.Item.Name,
- Id = i.Item.Id
- }).ToArray();
- }
- else
+ Name = i.Item.Name,
+ Id = i.Item.Id
+ }).ToArray();
+ }
+ else
+ {
+ filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
{
- filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
- {
- Name = i.Item.Name,
- Id = i.Item.Id
- }).ToArray();
- }
-
- return filters;
+ Name = i.Item.Name,
+ Id = i.Item.Id
+ }).ToArray();
}
+
+ return filters;
}
}
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 611643bd8..da60f2c60 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -18,194 +17,192 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Genre = MediaBrowser.Controller.Entities.Genre;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The genres controller.
+/// </summary>
+[Authorize]
+public class GenresController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// The genres controller.
+ /// Initializes a new instance of the <see cref="GenresController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class GenresController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public GenresController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="GenresController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public GenresController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- }
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Gets all genres from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">The search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
- /// <param name="userId">User id.</param>
- /// <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>
- /// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetGenres(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isFavorite,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [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)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
+ /// <summary>
+ /// Gets all genres from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">The search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
+ /// <param name="userId">User id.</param>
+ /// <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>
+ /// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetGenres(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ User? user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- var parentItem = _libraryManager.GetParentItem(parentId, userId);
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
- var query = new InternalItemsQuery(user)
- {
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
- {
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { parentId.Value };
- }
- else
- {
- query.ItemIds = new[] { parentId.Value };
- }
- }
-
- QueryResult<(BaseItem, ItemCounts)> result;
- if (parentItem is ICollectionFolder parentCollectionFolder
- && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
- || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
+ };
+
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- result = _libraryManager.GetMusicGenres(query);
+ query.AncestorIds = new[] { parentId.Value };
}
else
{
- result = _libraryManager.GetGenres(query);
+ query.ItemIds = new[] { parentId.Value };
}
-
- var shouldIncludeItemTypes = includeItemTypes.Length != 0;
- return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
- /// <summary>
- /// Gets a genre, by name.
- /// </summary>
- /// <param name="genreName">The genre name.</param>
- /// <param name="userId">The user id.</param>
- /// <response code="200">Genres returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the genre.</returns>
- [HttpGet("{genreName}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
+ QueryResult<(BaseItem, ItemCounts)> result;
+ if (parentItem is ICollectionFolder parentCollectionFolder
+ && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
+ || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
{
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
+ result = _libraryManager.GetMusicGenres(query);
+ }
+ else
+ {
+ result = _libraryManager.GetGenres(query);
+ }
- Genre? item;
- if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
- {
- item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
- }
- else
- {
- item = _libraryManager.GetGenre(genreName);
- }
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
+ }
- item ??= new Genre();
+ /// <summary>
+ /// Gets a genre, by name.
+ /// </summary>
+ /// <param name="genreName">The genre name.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">Genres returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the genre.</returns>
+ [HttpGet("{genreName}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
- if (userId is null || userId.Value.Equals(default))
- {
- return _dtoService.GetBaseItemDto(item, dtoOptions);
- }
+ Genre? item;
+ if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
+ {
+ item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
+ }
+ else
+ {
+ item = _libraryManager.GetGenre(genreName);
+ }
- var user = _userManager.GetUserById(userId.Value);
+ item ??= new Genre();
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
- where T : BaseItem, new()
+ private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
+ where T : BaseItem, new()
+ {
+ var result = libraryManager.GetItemList(new InternalItemsQuery
{
- var result = libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '&'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ Name = name.Replace(BaseItem.SlugChar, '&'),
+ IncludeItemTypes = new[] { baseItemKind },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
- result ??= libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '/'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '/'),
+ IncludeItemTypes = new[] { baseItemKind },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
- result ??= libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '?'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '?'),
+ IncludeItemTypes = new[] { baseItemKind },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
- return result;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 50fee233a..d7cec865e 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -15,178 +14,177 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The hls segment controller.
+/// </summary>
+[Route("")]
+public class HlsSegmentController : BaseJellyfinApiController
{
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
+ public HlsSegmentController(
+ IFileSystem fileSystem,
+ IServerConfigurationManager serverConfigurationManager,
+ TranscodingJobHelper transcodingJobHelper)
+ {
+ _fileSystem = fileSystem;
+ _serverConfigurationManager = serverConfigurationManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ }
+
/// <summary>
- /// The hls segment controller.
+ /// Gets the specified audio segment for an audio item.
/// </summary>
- [Route("")]
- public class HlsSegmentController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="segmentId">The segment id.</param>
+ /// <response code="200">Hls audio segment returned.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
+ // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
+ // [Authenticated]
+ [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
+ [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+ public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
- public HlsSegmentController(
- IFileSystem fileSystem,
- IServerConfigurationManager serverConfigurationManager,
- TranscodingJobHelper transcodingJobHelper)
+ // TODO: Deprecate with new iOS app
+ var file = segmentId + Path.GetExtension(Request.Path);
+ var transcodePath = _serverConfigurationManager.GetTranscodePath();
+ file = Path.GetFullPath(Path.Combine(transcodePath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
{
- _fileSystem = fileSystem;
- _serverConfigurationManager = serverConfigurationManager;
- _transcodingJobHelper = transcodingJobHelper;
+ return BadRequest("Invalid segment.");
}
- /// <summary>
- /// Gets the specified audio segment for an audio item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="segmentId">The segment id.</param>
- /// <response code="200">Hls audio segment returned.</response>
- /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
- // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
- // [Authenticated]
- [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
- [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
- public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
- {
- // TODO: Deprecate with new iOS app
- var file = segmentId + Path.GetExtension(Request.Path);
- var transcodePath = _serverConfigurationManager.GetTranscodePath();
- file = Path.GetFullPath(Path.Combine(transcodePath, file));
- var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
- {
- return BadRequest("Invalid segment.");
- }
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
+ }
- return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
+ /// <summary>
+ /// Gets a hls video playlist.
+ /// </summary>
+ /// <param name="itemId">The video id.</param>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <response code="200">Hls video playlist returned.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
+ [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+ public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
+ {
+ var file = playlistId + Path.GetExtension(Request.Path);
+ var transcodePath = _serverConfigurationManager.GetTranscodePath();
+ file = Path.GetFullPath(Path.Combine(transcodePath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
+ {
+ return BadRequest("Invalid segment.");
}
- /// <summary>
- /// Gets a hls video playlist.
- /// </summary>
- /// <param name="itemId">The video id.</param>
- /// <param name="playlistId">The playlist id.</param>
- /// <response code="200">Hls video playlist returned.</response>
- /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
- [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
- public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
- {
- var file = playlistId + Path.GetExtension(Request.Path);
- var transcodePath = _serverConfigurationManager.GetTranscodePath();
- file = Path.GetFullPath(Path.Combine(transcodePath, file));
- var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
- {
- return BadRequest("Invalid segment.");
- }
+ return GetFileResult(file, file);
+ }
- return GetFileResult(file, file);
- }
+ /// <summary>
+ /// Stops an active encoding.
+ /// </summary>
+ /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <response code="204">Encoding stopped successfully.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpDelete("Videos/ActiveEncodings")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult StopEncodingProcess(
+ [FromQuery, Required] string deviceId,
+ [FromQuery, Required] string playSessionId)
+ {
+ _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets a hls video segment.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="segmentId">The segment id.</param>
+ /// <param name="segmentContainer">The segment container.</param>
+ /// <response code="200">Hls video segment returned.</response>
+ /// <response code="404">Hls segment not found.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
+ // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
+ // [Authenticated]
+ [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesVideoFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+ public ActionResult GetHlsVideoSegmentLegacy(
+ [FromRoute, Required] string itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] string segmentId,
+ [FromRoute, Required] string segmentContainer)
+ {
+ var file = segmentId + Path.GetExtension(Request.Path);
+ var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
- /// <summary>
- /// Stops an active encoding.
- /// </summary>
- /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <response code="204">Encoding stopped successfully.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpDelete("Videos/ActiveEncodings")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult StopEncodingProcess(
- [FromQuery, Required] string deviceId,
- [FromQuery, Required] string playSessionId)
+ file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
{
- _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
- return NoContent();
+ return BadRequest("Invalid segment.");
}
- /// <summary>
- /// Gets a hls video segment.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="segmentId">The segment id.</param>
- /// <param name="segmentContainer">The segment container.</param>
- /// <response code="200">Hls video segment returned.</response>
- /// <response code="404">Hls segment not found.</response>
- /// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
- // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
- // [Authenticated]
- [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesVideoFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
- public ActionResult GetHlsVideoSegmentLegacy(
- [FromRoute, Required] string itemId,
- [FromRoute, Required] string playlistId,
- [FromRoute, Required] string segmentId,
- [FromRoute, Required] string segmentContainer)
- {
- var file = segmentId + Path.GetExtension(Request.Path);
- var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
+ var normalizedPlaylistId = playlistId;
- file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
- var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
+ var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
+ // Add . to start of segment container for future use.
+ segmentContainer = segmentContainer.Insert(0, ".");
+ string? playlistPath = null;
+ foreach (var path in filePaths)
+ {
+ var pathExtension = Path.GetExtension(path);
+ if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
+ && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
{
- return BadRequest("Invalid segment.");
+ playlistPath = path;
+ break;
}
+ }
- var normalizedPlaylistId = playlistId;
-
- var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
- // Add . to start of segment container for future use.
- segmentContainer = segmentContainer.Insert(0, ".");
- string? playlistPath = null;
- foreach (var path in filePaths)
- {
- var pathExtension = Path.GetExtension(path);
- if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
- || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
- && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
- {
- playlistPath = path;
- break;
- }
- }
+ return playlistPath is null
+ ? NotFound("Hls segment not found.")
+ : GetFileResult(file, playlistPath);
+ }
- return playlistPath is null
- ? NotFound("Hls segment not found.")
- : GetFileResult(file, playlistPath);
- }
+ private ActionResult GetFileResult(string path, string playlistPath)
+ {
+ var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
- private ActionResult GetFileResult(string path, string playlistPath)
+ Response.OnCompleted(() =>
{
- var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
-
- Response.OnCompleted(() =>
+ if (transcodingJob is not null)
{
- if (transcodingJob is not null)
- {
- _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
- }
+ _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+ }
- return Task.CompletedTask;
- });
+ return Task.CompletedTask;
+ });
- return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
- }
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
}
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index f866655c0..3c5f18af5 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -30,2116 +30,2115 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Image controller.
+/// </summary>
+[Route("")]
+public class ImageController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IImageProcessor _imageProcessor;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<ImageController> _logger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IApplicationPaths _appPaths;
+
/// <summary>
- /// Image controller.
+ /// Initializes a new instance of the <see cref="ImageController"/> class.
/// </summary>
- [Route("")]
- public class ImageController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ public ImageController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ IImageProcessor imageProcessor,
+ IFileSystem fileSystem,
+ ILogger<ImageController> logger,
+ IServerConfigurationManager serverConfigurationManager,
+ IApplicationPaths appPaths)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<ImageController> _logger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IApplicationPaths _appPaths;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ImageController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
- public ImageController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- IImageProcessor imageProcessor,
- IFileSystem fileSystem,
- ILogger<ImageController> logger,
- IServerConfigurationManager serverConfigurationManager,
- IApplicationPaths appPaths)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _imageProcessor = imageProcessor;
- _fileSystem = fileSystem;
- _logger = logger;
- _serverConfigurationManager = serverConfigurationManager;
- _appPaths = appPaths;
- }
-
- /// <summary>
- /// Sets the user image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image updated.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <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")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> PostUserImage(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? index = null)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
- }
-
- var user = _userManager.GetUserById(userId);
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
- if (user.ProfileImage is not null)
- {
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
- }
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _imageProcessor = imageProcessor;
+ _fileSystem = fileSystem;
+ _logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
+ _appPaths = appPaths;
+ }
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ /// <summary>
+ /// Sets the user image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image updated.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Users/{userId}/Images/{imageType}")]
+ [Authorize]
+ [AcceptsImageFile]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> PostUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? index = null)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
- .ConfigureAwait(false);
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
+ }
- return NoContent();
- }
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
+ {
+ return BadRequest("Incorrect ContentType.");
}
- /// <summary>
- /// Sets the user image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image updated.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <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")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> PostUserImageByIndex(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int index)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage is not null)
{
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- var user = _userManager.GetUserById(userId);
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
- if (user.ProfileImage is not null)
- {
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
- }
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ await _providerManager
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .ConfigureAwait(false);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
- .ConfigureAwait(false);
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ return NoContent();
+ }
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Sets the user image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image updated.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
+ [Authorize]
+ [AcceptsImageFile]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> PostUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Delete the user's image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Users/{userId}/Images/{imageType}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> DeleteUserImage(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? index = null)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
- }
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
+ }
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
- {
- return NoContent();
- }
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
+ {
+ return BadRequest("Incorrect ContentType.");
+ }
- try
- {
- System.IO.File.Delete(user.ProfileImage.Path);
- }
- catch (IOException e)
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage is not null)
{
- _logger.LogError(e, "Error deleting user profile image:");
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
+
+ await _providerManager
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .ConfigureAwait(false);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+
return NoContent();
}
+ }
- /// <summary>
- /// Delete the user's image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> DeleteUserImageByIndex(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int index)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
- }
+ /// <summary>
+ /// Delete the user's image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Users/{userId}/Images/{imageType}")]
+ [Authorize]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> DeleteUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? index = null)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
+ }
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
- {
- return NoContent();
- }
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
+ return NoContent();
+ }
- try
- {
- System.IO.File.Delete(user.ProfileImage.Path);
- }
- catch (IOException e)
- {
- _logger.LogError(e, "Error deleting user profile image:");
- }
+ try
+ {
+ System.IO.File.Delete(user.ProfileImage.Path);
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error deleting user profile image:");
+ }
+
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Delete the user's image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
+ [Authorize]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> DeleteUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
+ }
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
return NoContent();
}
- /// <summary>
- /// Delete an item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">The image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpDelete("Items/{itemId}/Images/{imageType}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteItemImage(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ try
+ {
+ System.IO.File.Delete(user.ProfileImage.Path);
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error deleting user profile image:");
+ }
- await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
- return NoContent();
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Delete an item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">The image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Items/{itemId}/Images/{imageType}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteItemImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Delete an item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">The image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteItemImageByIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
+ return NoContent();
+ }
- await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
- return NoContent();
+ /// <summary>
+ /// Delete an item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">The image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Set item image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <response code="204">Image saved.</response>
- /// <response code="404">Item not found.</response>
- /// <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")]
- public async Task<ActionResult> SetItemImage(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
+ return NoContent();
+ }
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+ /// <summary>
+ /// Set item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <response code="204">Image saved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <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.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> SetItemImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return NoContent();
- }
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
+ {
+ return BadRequest("Incorrect ContentType.");
}
- /// <summary>
- /// Set item image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">(Unused) Image index.</param>
- /// <response code="204">Image saved.</response>
- /// <response code="404">Item not found.</response>
- /// <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")]
- public async Task<ActionResult> SetItemImageByIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Set item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">(Unused) Image index.</param>
+ /// <response code="204">Image saved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <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.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> SetItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Updates the index for an item image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Old image index.</param>
- /// <param name="newIndex">New image index.</param>
- /// <response code="204">Image index updated.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateItemImageIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery, Required] int newIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
+ {
+ return BadRequest("Incorrect ContentType.");
+ }
+
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
return NoContent();
}
+ }
- /// <summary>
- /// Get item image infos.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Item images returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpGet("Items/{itemId}/Images")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Updates the index for an item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Old image index.</param>
+ /// <param name="newIndex">New image index.</param>
+ /// <response code="204">Image index updated.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateItemImageIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery, Required] int newIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- var list = new List<ImageInfo>();
- var itemImages = item.ImageInfos;
+ await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
+ return NoContent();
+ }
- if (itemImages.Length == 0)
- {
- // short-circuit
- return list;
- }
+ /// <summary>
+ /// Get item image infos.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Item images returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpGet("Items/{itemId}/Images")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct
+ var list = new List<ImageInfo>();
+ var itemImages = item.ImageInfos;
- foreach (var image in itemImages)
+ if (itemImages.Length == 0)
+ {
+ // short-circuit
+ return list;
+ }
+
+ await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct
+
+ foreach (var image in itemImages)
+ {
+ if (!item.AllowsMultipleImages(image.Type))
{
- if (!item.AllowsMultipleImages(image.Type))
- {
- var info = GetImageInfo(item, image, null);
+ var info = GetImageInfo(item, image, null);
- if (info is not null)
- {
- list.Add(info);
- }
+ if (info is not null)
+ {
+ list.Add(info);
}
}
+ }
- foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
- {
- var index = 0;
-
- // Prevent implicitly captured closure
- var currentImageType = imageType;
+ foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
+ {
+ var index = 0;
- foreach (var image in itemImages.Where(i => i.Type == currentImageType))
- {
- var info = GetImageInfo(item, image, index);
+ // Prevent implicitly captured closure
+ var currentImageType = imageType;
- if (info is not null)
- {
- list.Add(info);
- }
+ foreach (var image in itemImages.Where(i => i.Type == currentImageType))
+ {
+ var info = GetImageInfo(item, image, index);
- index++;
+ if (info is not null)
+ {
+ list.Add(info);
}
- }
- return list;
+ index++;
+ }
}
- /// <summary>
- /// Gets the item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <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>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Items/{itemId}/Images/{imageType}")]
- [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetItemImage(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] string? tag,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] ImageFormat? format,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ return list;
+ }
- return await GetImageInternal(
- itemId,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <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>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetItemImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery] string? tag,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets the item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <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>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetItemImageByIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int imageIndex,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] string? tag,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] ImageFormat? format,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- itemId,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <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>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery] string? tag,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets the item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <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>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
- [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetItemImage2(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int maxWidth,
- [FromRoute, Required] int maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromRoute, Required] string tag,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromRoute, Required] ImageFormat format,
- [FromQuery] bool? addPlayedIndicator,
- [FromRoute, Required] double percentPlayed,
- [FromRoute, Required] int unplayedCount,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromRoute, Required] int imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- itemId,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <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>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetItemImage2(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int maxWidth,
+ [FromRoute, Required] int maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromRoute, Required] string tag,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromRoute, Required] ImageFormat format,
+ [FromRoute, Required] double percentPlayed,
+ [FromRoute, Required] int unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute, Required] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get artist image by name.
- /// </summary>
- /// <param name="name">Artist name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetArtistImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromRoute, Required] int imageIndex)
- {
- var item = _libraryManager.GetArtist(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get artist image by name.
+ /// </summary>
+ /// <param name="name">Artist name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetArtistImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute, Required] int imageIndex)
+ {
+ var item = _libraryManager.GetArtist(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get genre image by name.
- /// </summary>
- /// <param name="name">Genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Genres/{name}/Images/{imageType}")]
- [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetGenreImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get genre image by name.
+ /// </summary>
+ /// <param name="name">Genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Genres/{name}/Images/{imageType}")]
+ [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetGenreImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get genre image by name.
- /// </summary>
- /// <param name="name">Genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetGenreImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get genre image by name.
+ /// </summary>
+ /// <param name="name">Genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetGenreImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get music genre image by name.
- /// </summary>
- /// <param name="name">Music genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("MusicGenres/{name}/Images/{imageType}")]
- [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetMusicGenreImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetMusicGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get music genre image by name.
+ /// </summary>
+ /// <param name="name">Music genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("MusicGenres/{name}/Images/{imageType}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetMusicGenreImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get music genre image by name.
- /// </summary>
- /// <param name="name">Music genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetMusicGenreImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetMusicGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get music genre image by name.
+ /// </summary>
+ /// <param name="name">Music genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetMusicGenreImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get person image by name.
- /// </summary>
- /// <param name="name">Person name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Persons/{name}/Images/{imageType}")]
- [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetPersonImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetPerson(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get person image by name.
+ /// </summary>
+ /// <param name="name">Person name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Persons/{name}/Images/{imageType}")]
+ [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetPersonImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetPerson(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get person image by name.
- /// </summary>
- /// <param name="name">Person name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetPersonImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetPerson(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get person image by name.
+ /// </summary>
+ /// <param name="name">Person name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetPersonImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetPerson(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get studio image by name.
- /// </summary>
- /// <param name="name">Studio name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Studios/{name}/Images/{imageType}")]
- [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetStudioImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetStudio(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get studio image by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Studios/{name}/Images/{imageType}")]
+ [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetStudioImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetStudio(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get studio image by name.
- /// </summary>
- /// <param name="name">Studio name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetStudioImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetStudio(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get studio image by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetStudioImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetStudio(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get user profile image.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Users/{userId}/Images/{imageType}")]
- [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetUserImage(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
- {
- return NotFound();
- }
-
- var info = new ItemImageInfo
- {
- Path = user.ProfileImage.Path,
- Type = ImageType.Profile,
- DateModified = user.ProfileImage.LastModified
- };
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- if (width.HasValue)
- {
- info.Width = width.Value;
- }
+ /// <summary>
+ /// Get user profile image.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Users/{userId}/Images/{imageType}")]
+ [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
+ return NotFound();
+ }
- if (height.HasValue)
- {
- info.Height = height.Value;
- }
+ var info = new ItemImageInfo
+ {
+ Path = user.ProfileImage.Path,
+ Type = ImageType.Profile,
+ DateModified = user.ProfileImage.LastModified
+ };
- return await GetImageInternal(
- user.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- null,
- info)
- .ConfigureAwait(false);
+ if (width.HasValue)
+ {
+ info.Width = width.Value;
}
- /// <summary>
- /// Get user profile image.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <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>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetUserImageByIndex(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
- {
- return NotFound();
- }
+ if (height.HasValue)
+ {
+ info.Height = height.Value;
+ }
- var info = new ItemImageInfo
- {
- Path = user.ProfileImage.Path,
- Type = ImageType.Profile,
- DateModified = user.ProfileImage.LastModified
- };
+ return await GetImageInternal(
+ user.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ null,
+ info)
+ .ConfigureAwait(false);
+ }
- if (width.HasValue)
- {
- info.Width = width.Value;
- }
+ /// <summary>
+ /// Get user profile image.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <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="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
+ return NotFound();
+ }
- if (height.HasValue)
- {
- info.Height = height.Value;
- }
+ var info = new ItemImageInfo
+ {
+ Path = user.ProfileImage.Path,
+ Type = ImageType.Profile,
+ DateModified = user.ProfileImage.LastModified
+ };
- return await GetImageInternal(
- user.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- null,
- info)
- .ConfigureAwait(false);
+ if (width.HasValue)
+ {
+ info.Width = width.Value;
}
- /// <summary>
- /// Generates or gets the splashscreen.
- /// </summary>
- /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="blur">Blur image.</param>
- /// <param name="backgroundColor">Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param>
- /// <param name="quality">Quality setting, from 0-100.</param>
- /// <response code="200">Splashscreen returned successfully.</response>
- /// <returns>The splashscreen.</returns>
- [HttpGet("Branding/Splashscreen")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesImageFile]
- public async Task<ActionResult> GetSplashscreen(
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery, Range(0, 100)] int quality = 90)
+ if (height.HasValue)
{
- var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- if (!brandingOptions.SplashscreenEnabled)
- {
- return NotFound();
- }
+ info.Height = height.Value;
+ }
- string splashscreenPath;
+ return await GetImageInternal(
+ user.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ null,
+ info)
+ .ConfigureAwait(false);
+ }
- if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
- && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
- {
- splashscreenPath = brandingOptions.SplashscreenLocation;
- }
- else
- {
- splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
- if (!System.IO.File.Exists(splashscreenPath))
- {
- return NotFound();
- }
- }
+ /// <summary>
+ /// Generates or gets the splashscreen.
+ /// </summary>
+ /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="blur">Blur image.</param>
+ /// <param name="backgroundColor">Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param>
+ /// <param name="quality">Quality setting, from 0-100.</param>
+ /// <response code="200">Splashscreen returned successfully.</response>
+ /// <returns>The splashscreen.</returns>
+ [HttpGet("Branding/Splashscreen")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetSplashscreen(
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery, Range(0, 100)] int quality = 90)
+ {
+ var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ if (!brandingOptions.SplashscreenEnabled)
+ {
+ return NotFound();
+ }
- var outputFormats = GetOutputFormats(format);
+ string splashscreenPath;
- TimeSpan? cacheDuration = null;
- if (!string.IsNullOrEmpty(tag))
+ if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
+ && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
+ {
+ splashscreenPath = brandingOptions.SplashscreenLocation;
+ }
+ else
+ {
+ splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
+ if (!System.IO.File.Exists(splashscreenPath))
{
- cacheDuration = TimeSpan.FromDays(365);
+ return NotFound();
}
-
- var options = new ImageProcessingOptions
- {
- Image = new ItemImageInfo
- {
- Path = splashscreenPath
- },
- Height = height,
- MaxHeight = maxHeight,
- MaxWidth = maxWidth,
- FillHeight = fillHeight,
- FillWidth = fillWidth,
- Quality = quality,
- Width = width,
- Blur = blur,
- BackgroundColor = backgroundColor,
- ForegroundLayer = foregroundLayer,
- SupportedOutputFormats = outputFormats
- };
-
- return await GetImageResult(
- options,
- cacheDuration,
- ImmutableDictionary<string, string>.Empty)
- .ConfigureAwait(false);
}
- /// <summary>
- /// Uploads a custom splashscreen.
- /// The body is expected to the image contents base64 encoded.
- /// </summary>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- /// <response code="204">Successfully uploaded new splashscreen.</response>
- /// <response code="400">Error reading MimeType from uploaded image.</response>
- /// <response code="403">User does not have permission to upload splashscreen..</response>
- /// <exception cref="ArgumentException">Error reading the image format.</exception>
- [HttpPost("Branding/Splashscreen")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [AcceptsImageFile]
- public async Task<ActionResult> UploadCustomSplashscreen()
- {
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
-
- if (!mimeType.HasValue)
- {
- return BadRequest("Error reading mimetype from uploaded image");
- }
+ var outputFormats = GetOutputFormats(format);
- var extension = MimeTypes.ToExtension(mimeType.Value);
- if (string.IsNullOrEmpty(extension))
- {
- return BadRequest("Error converting mimetype to an image extension");
- }
-
- var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
- var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- brandingOptions.SplashscreenLocation = filePath;
- _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
+ TimeSpan? cacheDuration = null;
+ if (!string.IsNullOrEmpty(tag))
+ {
+ cacheDuration = TimeSpan.FromDays(365);
+ }
- var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await using (fs.ConfigureAwait(false))
- {
- await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
- }
+ var options = new ImageProcessingOptions
+ {
+ Image = new ItemImageInfo
+ {
+ Path = splashscreenPath
+ },
+ Height = height,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ FillHeight = fillHeight,
+ FillWidth = fillWidth,
+ Quality = quality,
+ Width = width,
+ Blur = blur,
+ BackgroundColor = backgroundColor,
+ ForegroundLayer = foregroundLayer,
+ SupportedOutputFormats = outputFormats
+ };
+
+ return await GetImageResult(
+ options,
+ cacheDuration,
+ ImmutableDictionary<string, string>.Empty)
+ .ConfigureAwait(false);
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Uploads a custom splashscreen.
+ /// The body is expected to the image contents base64 encoded.
+ /// </summary>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ /// <response code="204">Successfully uploaded new splashscreen.</response>
+ /// <response code="400">Error reading MimeType from uploaded image.</response>
+ /// <response code="403">User does not have permission to upload splashscreen..</response>
+ /// <exception cref="ArgumentException">Error reading the image format.</exception>
+ [HttpPost("Branding/Splashscreen")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [AcceptsImageFile]
+ public async Task<ActionResult> UploadCustomSplashscreen()
+ {
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
+ {
+ return BadRequest("Incorrect ContentType.");
}
- /// <summary>
- /// Delete a custom splashscreen.
- /// </summary>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- /// <response code="204">Successfully deleted the custom splashscreen.</response>
- /// <response code="403">User does not have permission to delete splashscreen..</response>
- [HttpDelete("Branding/Splashscreen")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult DeleteCustomSplashscreen()
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
{
+ var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
- && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
+ brandingOptions.SplashscreenLocation = filePath;
+ _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
+
+ var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (fs.ConfigureAwait(false))
{
- System.IO.File.Delete(brandingOptions.SplashscreenLocation);
- brandingOptions.SplashscreenLocation = null;
- _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
+ await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
}
return NoContent();
}
+ }
- private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
+ /// <summary>
+ /// Delete a custom splashscreen.
+ /// </summary>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ /// <response code="204">Successfully deleted the custom splashscreen.</response>
+ /// <response code="403">User does not have permission to delete splashscreen..</response>
+ [HttpDelete("Branding/Splashscreen")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult DeleteCustomSplashscreen()
+ {
+ var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
+ && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
{
- using var reader = new StreamReader(inputStream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- var bytes = Convert.FromBase64String(text);
- return new MemoryStream(bytes, 0, bytes.Length, false, true);
+ System.IO.File.Delete(brandingOptions.SplashscreenLocation);
+ brandingOptions.SplashscreenLocation = null;
+ _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
}
- private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
- {
- int? width = null;
- int? height = null;
- string? blurhash = null;
- long length = 0;
+ return NoContent();
+ }
- try
- {
- if (info.IsLocalFile)
- {
- var fileInfo = _fileSystem.GetFileInfo(info.Path);
- length = fileInfo.Length;
-
- blurhash = info.BlurHash;
- width = info.Width;
- height = info.Height;
-
- if (width <= 0 || height <= 0)
- {
- width = null;
- height = null;
- }
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
- }
+ private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
+ {
+ using var reader = new StreamReader(inputStream);
+ var text = await reader.ReadToEndAsync().ConfigureAwait(false);
- try
- {
- return new ImageInfo
- {
- Path = info.Path,
- ImageIndex = imageIndex,
- ImageType = info.Type,
- ImageTag = _imageProcessor.GetImageCacheTag(item, info),
- Size = length,
- BlurHash = blurhash,
- Width = width,
- Height = height
- };
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
- return null;
- }
- }
+ var bytes = Convert.FromBase64String(text);
+ return new MemoryStream(bytes, 0, bytes.Length, false, true);
+ }
- private async Task<ActionResult> GetImageInternal(
- Guid itemId,
- ImageType imageType,
- int? imageIndex,
- string? tag,
- ImageFormat? format,
- int? maxWidth,
- int? maxHeight,
- double? percentPlayed,
- int? unplayedCount,
- int? width,
- int? height,
- int? quality,
- int? fillWidth,
- int? fillHeight,
- bool? addPlayedIndicator,
- int? blur,
- string? backgroundColor,
- string? foregroundLayer,
- BaseItem? item,
- ItemImageInfo? imageInfo = null)
- {
- if (percentPlayed.HasValue)
+ private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
+ {
+ int? width = null;
+ int? height = null;
+ string? blurhash = null;
+ long length = 0;
+
+ try
+ {
+ if (info.IsLocalFile)
{
- if (percentPlayed.Value <= 0)
- {
- percentPlayed = null;
- }
- else if (percentPlayed.Value >= 100)
+ var fileInfo = _fileSystem.GetFileInfo(info.Path);
+ length = fileInfo.Length;
+
+ blurhash = info.BlurHash;
+ width = info.Width;
+ height = info.Height;
+
+ if (width <= 0 || height <= 0)
{
- percentPlayed = null;
- addPlayedIndicator = true;
+ width = null;
+ height = null;
}
}
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
+ }
- if (percentPlayed.HasValue)
- {
- unplayedCount = null;
- }
+ try
+ {
+ return new ImageInfo
+ {
+ Path = info.Path,
+ ImageIndex = imageIndex,
+ ImageType = info.Type,
+ ImageTag = _imageProcessor.GetImageCacheTag(item, info),
+ Size = length,
+ BlurHash = blurhash,
+ Width = width,
+ Height = height
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
+ return null;
+ }
+ }
- if (unplayedCount.HasValue
- && unplayedCount.Value <= 0)
+ private async Task<ActionResult> GetImageInternal(
+ Guid itemId,
+ ImageType imageType,
+ int? imageIndex,
+ string? tag,
+ ImageFormat? format,
+ int? maxWidth,
+ int? maxHeight,
+ double? percentPlayed,
+ int? unplayedCount,
+ int? width,
+ int? height,
+ int? quality,
+ int? fillWidth,
+ int? fillHeight,
+ int? blur,
+ string? backgroundColor,
+ string? foregroundLayer,
+ BaseItem? item,
+ ItemImageInfo? imageInfo = null)
+ {
+ if (percentPlayed.HasValue)
+ {
+ if (percentPlayed.Value <= 0)
{
- unplayedCount = null;
+ percentPlayed = null;
}
-
- if (imageInfo is null)
+ else if (percentPlayed.Value >= 100)
{
- imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
- if (imageInfo is null)
- {
- return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType));
- }
+ percentPlayed = null;
}
+ }
- var outputFormats = GetOutputFormats(format);
+ if (percentPlayed.HasValue)
+ {
+ unplayedCount = null;
+ }
- TimeSpan? cacheDuration = null;
+ if (unplayedCount.HasValue
+ && unplayedCount.Value <= 0)
+ {
+ unplayedCount = null;
+ }
- if (!string.IsNullOrEmpty(tag))
+ if (imageInfo is null)
+ {
+ imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
+ if (imageInfo is null)
{
- cacheDuration = TimeSpan.FromDays(365);
+ return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType));
}
+ }
- var responseHeaders = new Dictionary<string, string>
- {
- { "transferMode.dlna.org", "Interactive" },
- { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
- };
+ var outputFormats = GetOutputFormats(format);
- if (!imageInfo.IsLocalFile && item is not null)
- {
- imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false);
- }
+ TimeSpan? cacheDuration = null;
- var options = new ImageProcessingOptions
- {
- Height = height,
- ImageIndex = imageIndex ?? 0,
- Image = imageInfo,
- Item = item,
- ItemId = itemId,
- MaxHeight = maxHeight,
- MaxWidth = maxWidth,
- FillHeight = fillHeight,
- FillWidth = fillWidth,
- Quality = quality ?? 100,
- Width = width,
- AddPlayedIndicator = addPlayedIndicator ?? false,
- PercentPlayed = percentPlayed ?? 0,
- UnplayedCount = unplayedCount,
- Blur = blur,
- BackgroundColor = backgroundColor,
- ForegroundLayer = foregroundLayer,
- SupportedOutputFormats = outputFormats
- };
+ if (!string.IsNullOrEmpty(tag))
+ {
+ cacheDuration = TimeSpan.FromDays(365);
+ }
- return await GetImageResult(
- options,
- cacheDuration,
- responseHeaders).ConfigureAwait(false);
+ var responseHeaders = new Dictionary<string, string>
+ {
+ { "transferMode.dlna.org", "Interactive" },
+ { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
+ };
+
+ if (!imageInfo.IsLocalFile && item is not null)
+ {
+ imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false);
}
- private ImageFormat[] GetOutputFormats(ImageFormat? format)
+ var options = new ImageProcessingOptions
{
- if (format.HasValue)
- {
- return new[] { format.Value };
- }
+ Height = height,
+ ImageIndex = imageIndex ?? 0,
+ Image = imageInfo,
+ Item = item,
+ ItemId = itemId,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ FillHeight = fillHeight,
+ FillWidth = fillWidth,
+ Quality = quality ?? 100,
+ Width = width,
+ PercentPlayed = percentPlayed ?? 0,
+ UnplayedCount = unplayedCount,
+ Blur = blur,
+ BackgroundColor = backgroundColor,
+ ForegroundLayer = foregroundLayer,
+ SupportedOutputFormats = outputFormats
+ };
+
+ return await GetImageResult(
+ options,
+ cacheDuration,
+ responseHeaders).ConfigureAwait(false);
+ }
- return GetClientSupportedFormats();
+ private ImageFormat[] GetOutputFormats(ImageFormat? format)
+ {
+ if (format.HasValue)
+ {
+ return new[] { format.Value };
}
- private ImageFormat[] GetClientSupportedFormats()
+ return GetClientSupportedFormats();
+ }
+
+ private ImageFormat[] GetClientSupportedFormats()
+ {
+ var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+ for (var i = 0; i < supportedFormats.Length; i++)
{
- var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
- for (var i = 0; i < supportedFormats.Length; i++)
+ // Remove charsets etc. (anything after semi-colon)
+ var type = supportedFormats[i];
+ int index = type.IndexOf(';', StringComparison.Ordinal);
+ if (index != -1)
{
- // Remove charsets etc. (anything after semi-colon)
- var type = supportedFormats[i];
- int index = type.IndexOf(';', StringComparison.Ordinal);
- if (index != -1)
- {
- supportedFormats[i] = type.Substring(0, index);
- }
+ supportedFormats[i] = type.Substring(0, index);
}
+ }
- var acceptParam = Request.Query[HeaderNames.Accept];
+ var acceptParam = Request.Query[HeaderNames.Accept];
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
- if (!supportsWebP)
+ if (!supportsWebP)
+ {
+ var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
+ if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
+ && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
{
- var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
- if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
- && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
- {
- supportsWebP = true;
- }
+ supportsWebP = true;
}
+ }
- var formats = new List<ImageFormat>(4);
+ var formats = new List<ImageFormat>(4);
- if (supportsWebP)
- {
- formats.Add(ImageFormat.Webp);
- }
+ if (supportsWebP)
+ {
+ formats.Add(ImageFormat.Webp);
+ }
- formats.Add(ImageFormat.Jpg);
- formats.Add(ImageFormat.Png);
+ formats.Add(ImageFormat.Jpg);
+ formats.Add(ImageFormat.Png);
- if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
- {
- formats.Add(ImageFormat.Gif);
- }
+ if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
+ {
+ formats.Add(ImageFormat.Gif);
+ }
+
+ return formats.ToArray();
+ }
- return formats.ToArray();
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll)
+ {
+ if (requestAcceptTypes.Contains(format.GetMimeType()))
+ {
+ return true;
}
- private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll)
+ if (acceptAll && requestAcceptTypes.Contains("*/*"))
{
- if (requestAcceptTypes.Contains(format.GetMimeType()))
- {
- return true;
- }
+ return true;
+ }
- if (acceptAll && requestAcceptTypes.Contains("*/*"))
- {
- return true;
- }
+ // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
+ var normalized = format.ToString().ToLowerInvariant();
+ return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
+ }
- // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
- var normalized = format.ToString().ToLowerInvariant();
- return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
+ private async Task<ActionResult> GetImageResult(
+ ImageProcessingOptions imageProcessingOptions,
+ TimeSpan? cacheDuration,
+ IDictionary<string, string> headers)
+ {
+ var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false);
+
+ var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
+ var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
+
+ // if the parsing of the IfModifiedSince header was not successful, disable caching
+ if (!parsingSuccessful)
+ {
+ // disableCaching = true;
}
- private async Task<ActionResult> GetImageResult(
- ImageProcessingOptions imageProcessingOptions,
- TimeSpan? cacheDuration,
- IDictionary<string, string> headers)
+ foreach (var (key, value) in headers)
{
- var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false);
+ Response.Headers.Add(key, value);
+ }
- var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
- var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
+ Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
+ Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
+ Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
- // if the parsing of the IfModifiedSince header was not successful, disable caching
- if (!parsingSuccessful)
+ if (disableCaching)
+ {
+ Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
+ Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
+ }
+ else
+ {
+ if (cacheDuration.HasValue)
{
- // disableCaching = true;
+ Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
}
-
- foreach (var (key, value) in headers)
+ else
{
- Response.Headers.Add(key, value);
+ Response.Headers.Add(HeaderNames.CacheControl, "public");
}
- Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
- Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
- Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
+ Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
- if (disableCaching)
- {
- Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
- Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
- }
- else
+ // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
+ if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
{
- if (cacheDuration.HasValue)
- {
- Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
- }
- else
+ if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
{
- Response.Headers.Add(HeaderNames.CacheControl, "public");
+ Response.StatusCode = StatusCodes.Status304NotModified;
+ return new ContentResult();
}
+ }
+ }
- Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
+ return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
+ }
- // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
- if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
- {
- if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
- {
- Response.StatusCode = StatusCodes.Status304NotModified;
- return new ContentResult();
- }
- }
- }
+ internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extension)
+ {
+ extension = null;
+ if (string.IsNullOrEmpty(contentType))
+ {
+ return false;
+ }
- return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
+ if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
+ && parsedValue.MediaType.HasValue
+ && MimeTypes.IsImage(parsedValue.MediaType.Value))
+ {
+ extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
+ return extension is not null;
}
+
+ return false;
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 2e0d3cb99..4dc2a4253 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
@@ -16,346 +16,352 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The instant mix controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class InstantMixController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMusicManager _musicManager;
+
/// <summary>
- /// The instant mix controller.
+ /// Initializes a new instance of the <see cref="InstantMixController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class InstantMixController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public InstantMixController(
+ IUserManager userManager,
+ IDtoService dtoService,
+ IMusicManager musicManager,
+ ILibraryManager libraryManager)
{
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly ILibraryManager _libraryManager;
- private readonly IMusicManager _musicManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="InstantMixController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public InstantMixController(
- IUserManager userManager,
- IDtoService dtoService,
- IMusicManager musicManager,
- ILibraryManager libraryManager)
- {
- _userManager = userManager;
- _dtoService = dtoService;
- _musicManager = musicManager;
- _libraryManager = libraryManager;
- }
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _musicManager = musicManager;
+ _libraryManager = libraryManager;
+ }
- /// <summary>
- /// Creates an instant playlist based on a given song.
- /// </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("Songs/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
- [FromRoute, 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)
- {
- var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <summary>
+ /// Creates an instant playlist based on a given song.
+ /// </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("Songs/{id}/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
+ [FromRoute, 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)
+ {
+ var item = _libraryManager.GetItemById(id);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+ return GetResult(items, user, limit, dtoOptions);
+ }
- /// <summary>
- /// 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>
- /// <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("Albums/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
- [FromRoute, 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)
- {
- var album = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <summary>
+ /// 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>
+ /// <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("Albums/{id}/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
+ [FromRoute, 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)
+ {
+ var album = _libraryManager.GetItemById(id);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
+ return GetResult(items, user, limit, dtoOptions);
+ }
- /// <summary>
- /// 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>
- /// <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("Playlists/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
- [FromRoute, 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)
- {
- var playlist = (Playlist)_libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <summary>
+ /// 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>
+ /// <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("Playlists/{id}/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
+ [FromRoute, 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)
+ {
+ var playlist = (Playlist)_libraryManager.GetItemById(id);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
+ return GetResult(items, user, limit, dtoOptions);
+ }
- /// <summary>
- /// 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>
- /// <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/{name}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
- [FromRoute, Required] string name,
- [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)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <summary>
+ /// 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>
+ /// <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/{name}/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
+ [FromRoute, Required] string name,
+ [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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
+ 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/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
- [FromRoute, 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)
- {
- var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
- 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/{id}/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
+ [FromRoute, 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)
+ {
+ var item = _libraryManager.GetItemById(id);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+ return GetResult(items, user, limit, dtoOptions);
+ }
- /// <summary>
- /// 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>
- /// <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("Items/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
- [FromRoute, 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)
- {
- var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <summary>
+ /// 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>
+ /// <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("Items/{id}/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
+ [FromRoute, 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)
+ {
+ var item = _libraryManager.GetItemById(id);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+ 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 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)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
- [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)
- {
- var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <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)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
+ [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)
+ {
+ var item = _libraryManager.GetItemById(id);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+ return GetResult(items, user, limit, dtoOptions);
+ }
- private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
- {
- var list = items;
+ private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
+ {
+ var list = items;
- var totalCount = list.Count;
+ var totalCount = list.Count;
- if (limit.HasValue && limit < list.Count)
- {
- list = list.GetRange(0, limit.Value);
- }
+ if (limit.HasValue && limit < list.Count)
+ {
+ list = list.GetRange(0, limit.Value);
+ }
- var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
+ var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
- var result = new QueryResult<BaseItemDto>(
- 0,
- totalCount,
- returnList);
+ var result = new QueryResult<BaseItemDto>(
+ 0,
+ totalCount,
+ returnList);
- return result;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 34893d682..b030e74dd 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -18,257 +17,256 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Item lookup controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class ItemLookupController : BaseJellyfinApiController
{
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<ItemLookupController> _logger;
+
/// <summary>
- /// Item lookup controller.
+ /// Initializes a new instance of the <see cref="ItemLookupController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ItemLookupController : BaseJellyfinApiController
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
+ public ItemLookupController(
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ ILogger<ItemLookupController> logger)
{
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<ItemLookupController> _logger;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemLookupController"/> class.
- /// </summary>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
- public ItemLookupController(
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager,
- ILogger<ItemLookupController> logger)
+ /// <summary>
+ /// Get the item's external id info.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">External id info retrieved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>List of external id info.</returns>
+ [HttpGet("Items/{itemId}/ExternalIdInfos")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- _libraryManager = libraryManager;
- _logger = logger;
+ return NotFound();
}
- /// <summary>
- /// Get the item's external id info.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">External id info retrieved.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>List of external id info.</returns>
- [HttpGet("Items/{itemId}/ExternalIdInfos")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
-
- return Ok(_providerManager.GetExternalIdInfos(item));
- }
+ return Ok(_providerManager.GetExternalIdInfos(item));
+ }
- /// <summary>
- /// Get movie remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Movie remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/Movie")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get movie remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Movie remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/Movie")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get trailer remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Trailer remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/Trailer")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get trailer remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Trailer remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/Trailer")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get music video remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Music video remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/MusicVideo")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get music video remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Music video remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/MusicVideo")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get series remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Series remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/Series")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get series remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Series remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/Series")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get box set remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Box set remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/BoxSet")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get box set remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Box set remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/BoxSet")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get music artist remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Music artist remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/MusicArtist")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get music artist remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Music artist remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/MusicArtist")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get music album remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Music album remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/MusicAlbum")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get music album remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Music album remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/MusicAlbum")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get person remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Person remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/Person")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get person remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Person remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/Person")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Get book remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Book remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/Book")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get book remote search.
+ /// </summary>
+ /// <param name="query">Remote search query.</param>
+ /// <response code="200">Book remote search executed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/Book")]
+ public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query)
+ {
+ var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
+ .ConfigureAwait(false);
+ return Ok(results);
+ }
- /// <summary>
- /// Applies search criteria to an item and refreshes metadata.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="searchResult">The remote search result.</param>
- /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
- /// <response code="204">Item metadata refreshed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="NoContentResult"/>.
- /// </returns>
- [HttpPost("Items/RemoteSearch/Apply/{itemId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> ApplySearchCriteria(
- [FromRoute, Required] Guid itemId,
- [FromBody, Required] RemoteSearchResult searchResult,
- [FromQuery] bool replaceAllImages = true)
- {
- var item = _libraryManager.GetItemById(itemId);
- _logger.LogInformation(
- "Setting provider id's to item {0}-{1}: {2}",
- item.Id,
- item.Name,
- JsonSerializer.Serialize(searchResult.ProviderIds));
+ /// <summary>
+ /// Applies search criteria to an item and refreshes metadata.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="searchResult">The remote search result.</param>
+ /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
+ /// <response code="204">Item metadata refreshed.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="NoContentResult"/>.
+ /// </returns>
+ [HttpPost("Items/RemoteSearch/Apply/{itemId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> ApplySearchCriteria(
+ [FromRoute, Required] Guid itemId,
+ [FromBody, Required] RemoteSearchResult searchResult,
+ [FromQuery] bool replaceAllImages = true)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ _logger.LogInformation(
+ "Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
+ item.Id,
+ item.Name,
+ searchResult.ProviderIds);
- // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
- item.ProviderIds = searchResult.ProviderIds;
- await _providerManager.RefreshFullItem(
- item,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ReplaceAllMetadata = true,
- ReplaceAllImages = replaceAllImages,
- SearchResult = searchResult,
- RemoveOldMetadata = true
- },
- CancellationToken.None).ConfigureAwait(false);
+ // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
+ item.ProviderIds = searchResult.ProviderIds;
+ await _providerManager.RefreshFullItem(
+ item,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllMetadata = true,
+ ReplaceAllImages = replaceAllImages,
+ SearchResult = searchResult,
+ RemoveOldMetadata = true
+ },
+ CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 0dc3fbd05..b8f6e91ad 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -9,78 +9,77 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Item Refresh Controller.
+/// </summary>
+[Route("Items")]
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ItemRefreshController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+
/// <summary>
- /// Item Refresh Controller.
+ /// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
/// </summary>
- [Route("Items")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ItemRefreshController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+ public ItemRefreshController(
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ IFileSystem fileSystem)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
- public ItemRefreshController(
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- IFileSystem fileSystem)
+ /// <summary>
+ /// Refreshes metadata for an item.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
+ /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
+ /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <response code="204">Item metadata refresh queued.</response>
+ /// <response code="404">Item to refresh not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+ [HttpPost("{itemId}/Refresh")]
+ [Description("Refreshes metadata for an item.")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult RefreshItem(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
+ [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
+ [FromQuery] bool replaceAllMetadata = false,
+ [FromQuery] bool replaceAllImages = false)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
+ return NotFound();
}
- /// <summary>
- /// Refreshes metadata for an item.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
- /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
- /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
- /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
- /// <response code="204">Item metadata refresh queued.</response>
- /// <response code="404">Item to refresh not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
- [HttpPost("{itemId}/Refresh")]
- [Description("Refreshes metadata for an item.")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult RefreshItem(
- [FromRoute, Required] Guid itemId,
- [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
- [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
- [FromQuery] bool replaceAllMetadata = false,
- [FromQuery] bool replaceAllImages = false)
+ var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ MetadataRefreshMode = metadataRefreshMode,
+ ImageRefreshMode = imageRefreshMode,
+ ReplaceAllImages = replaceAllImages,
+ ReplaceAllMetadata = replaceAllMetadata,
+ ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
+ || imageRefreshMode == MetadataRefreshMode.FullRefresh
+ || replaceAllImages
+ || replaceAllMetadata,
+ IsAutomated = false
+ };
- var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = metadataRefreshMode,
- ImageRefreshMode = imageRefreshMode,
- ReplaceAllImages = replaceAllImages,
- ReplaceAllMetadata = replaceAllMetadata,
- ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
- || imageRefreshMode == MetadataRefreshMode.FullRefresh
- || replaceAllImages
- || replaceAllMetadata,
- IsAutomated = false
- };
-
- _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
- return NoContent();
- }
+ _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index af3d779f5..ece053a9a 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -20,332 +20,378 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Item update controller.
+/// </summary>
+[Route("")]
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ItemUpdateController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly ILocalizationManager _localizationManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ItemUpdateController(
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ ILocalizationManager localizationManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _localizationManager = localizationManager;
+ _fileSystem = fileSystem;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
/// <summary>
- /// Item update controller.
+ /// Updates an item.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ItemUpdateController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="request">The new item properties.</param>
+ /// <response code="204">Item updated.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+ [HttpPost("Items/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly ILocalizationManager _localizationManager;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public ItemUpdateController(
- IFileSystem fileSystem,
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- ILocalizationManager localizationManager,
- IServerConfigurationManager serverConfigurationManager)
- {
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _localizationManager = localizationManager;
- _fileSystem = fileSystem;
- _serverConfigurationManager = serverConfigurationManager;
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Updates an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="request">The new item properties.</param>
- /// <response code="204">Item updated.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
- [HttpPost("Items/{itemId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ var newLockData = request.LockData ?? false;
+ var isLockedChanged = item.IsLocked != newLockData;
- var newLockData = request.LockData ?? false;
- var isLockedChanged = item.IsLocked != newLockData;
+ var series = item as Series;
+ var displayOrderChanged = series is not null && !string.Equals(
+ series.DisplayOrder ?? string.Empty,
+ request.DisplayOrder ?? string.Empty,
+ StringComparison.OrdinalIgnoreCase);
- var series = item as Series;
- var displayOrderChanged = series is not null && !string.Equals(
- series.DisplayOrder ?? string.Empty,
- request.DisplayOrder ?? string.Empty,
- StringComparison.OrdinalIgnoreCase);
+ // Do this first so that metadata savers can pull the updates from the database.
+ if (request.People is not null)
+ {
+ _libraryManager.UpdatePeople(
+ item,
+ request.People.Select(x => new PersonInfo
+ {
+ Name = x.Name,
+ Role = x.Role,
+ Type = x.Type
+ }).ToList());
+ }
- // Do this first so that metadata savers can pull the updates from the database.
- if (request.People is not null)
- {
- _libraryManager.UpdatePeople(
- item,
- request.People.Select(x => new PersonInfo
- {
- Name = x.Name,
- Role = x.Role,
- Type = x.Type
- }).ToList());
- }
+ await UpdateItem(request, item).ConfigureAwait(false);
- UpdateItem(request, item);
+ item.OnMetadataChanged();
- item.OnMetadataChanged();
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ if (isLockedChanged && item.IsFolder)
+ {
+ var folder = (Folder)item;
- if (isLockedChanged && item.IsFolder)
+ foreach (var child in folder.GetRecursiveChildren())
{
- var folder = (Folder)item;
+ child.IsLocked = newLockData;
+ await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ }
+ }
- foreach (var child in folder.GetRecursiveChildren())
+ if (displayOrderChanged)
+ {
+ _providerManager.QueueRefresh(
+ series!.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
- child.IsLocked = newLockData;
- await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- }
- }
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllMetadata = true
+ },
+ RefreshPriority.High);
+ }
- if (displayOrderChanged)
- {
- _providerManager.QueueRefresh(
- series!.Id,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ReplaceAllMetadata = true
- },
- RefreshPriority.High);
- }
+ return NoContent();
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Gets metadata editor info for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="200">Item metadata editor returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+ [HttpGet("Items/{itemId}/MetadataEditor")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
- /// <summary>
- /// Gets metadata editor info for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="200">Item metadata editor returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
- [HttpGet("Items/{itemId}/MetadataEditor")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
-
- var info = new MetadataEditorInfo
- {
- ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
- ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
- Countries = _localizationManager.GetCountries().ToArray(),
- Cultures = _localizationManager.GetCultures().ToArray()
- };
-
- if (!item.IsVirtualItem
- && item is not ICollectionFolder
- && item is not UserView
- && item is not AggregateFolder
- && item is not LiveTvChannel
- && item is not IItemByName
- && item.SourceType == SourceType.Library)
+ var info = new MetadataEditorInfo
+ {
+ ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
+ ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
+ Countries = _localizationManager.GetCountries().ToArray(),
+ Cultures = _localizationManager.GetCultures().ToArray()
+ };
+
+ if (!item.IsVirtualItem
+ && item is not ICollectionFolder
+ && item is not UserView
+ && item is not AggregateFolder
+ && item is not LiveTvChannel
+ && item is not IItemByName
+ && item.SourceType == SourceType.Library)
+ {
+ var inheritedContentType = _libraryManager.GetInheritedContentType(item);
+ var configuredContentType = _libraryManager.GetConfiguredContentType(item);
+
+ if (string.IsNullOrWhiteSpace(inheritedContentType) ||
+ !string.IsNullOrWhiteSpace(configuredContentType))
{
- var inheritedContentType = _libraryManager.GetInheritedContentType(item);
- var configuredContentType = _libraryManager.GetConfiguredContentType(item);
+ info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
+ info.ContentType = configuredContentType;
- if (string.IsNullOrWhiteSpace(inheritedContentType) ||
- !string.IsNullOrWhiteSpace(configuredContentType))
+ if (string.IsNullOrWhiteSpace(inheritedContentType)
+ || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
- info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
- info.ContentType = configuredContentType;
-
- if (string.IsNullOrWhiteSpace(inheritedContentType)
- || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
- {
- info.ContentTypeOptions = info.ContentTypeOptions
- .Where(i => string.IsNullOrWhiteSpace(i.Value)
- || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
- .ToArray();
- }
+ info.ContentTypeOptions = info.ContentTypeOptions
+ .Where(i => string.IsNullOrWhiteSpace(i.Value)
+ || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
}
}
+ }
- return info;
+ return info;
+ }
+
+ /// <summary>
+ /// Updates an item's content type.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="contentType">The content type of the item.</param>
+ /// <response code="204">Item content type updated.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+ [HttpPost("Items/{itemId}/ContentType")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Updates an item's content type.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="contentType">The content type of the item.</param>
- /// <response code="204">Item content type updated.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
- [HttpPost("Items/{itemId}/ContentType")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
+ var path = item.ContainingFolderPath;
+
+ var types = _serverConfigurationManager.Configuration.ContentTypes
+ .Where(i => !string.IsNullOrWhiteSpace(i.Name))
+ .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (!string.IsNullOrWhiteSpace(contentType))
+ {
+ types.Add(new NameValuePair
{
- return NotFound();
- }
+ Name = path,
+ Value = contentType
+ });
+ }
- var path = item.ContainingFolderPath;
+ _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
+ _serverConfigurationManager.SaveConfiguration();
+ return NoContent();
+ }
- var types = _serverConfigurationManager.Configuration.ContentTypes
- .Where(i => !string.IsNullOrWhiteSpace(i.Name))
- .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
- .ToList();
+ private async Task UpdateItem(BaseItemDto request, BaseItem item)
+ {
+ item.Name = request.Name;
+ item.ForcedSortName = request.ForcedSortName;
- if (!string.IsNullOrWhiteSpace(contentType))
- {
- types.Add(new NameValuePair
- {
- Name = path,
- Value = contentType
- });
- }
+ item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
+
+ item.CriticRating = request.CriticRating;
+
+ item.CommunityRating = request.CommunityRating;
+ item.IndexNumber = request.IndexNumber;
+ item.ParentIndexNumber = request.ParentIndexNumber;
+ item.Overview = request.Overview;
+ item.Genres = request.Genres;
- _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
- _serverConfigurationManager.SaveConfiguration();
- return NoContent();
+ if (item is Episode episode)
+ {
+ episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
+ episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
+ episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
}
- private void UpdateItem(BaseItemDto request, BaseItem item)
+ if (request.Height is not null && item is LiveTvChannel channel)
{
- item.Name = request.Name;
- item.ForcedSortName = request.ForcedSortName;
+ channel.Height = request.Height.Value;
+ }
- item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
+ item.Tags = request.Tags;
- item.CriticRating = request.CriticRating;
+ if (request.Taglines is not null)
+ {
+ item.Tagline = request.Taglines.FirstOrDefault();
+ }
- item.CommunityRating = request.CommunityRating;
- item.IndexNumber = request.IndexNumber;
- item.ParentIndexNumber = request.ParentIndexNumber;
- item.Overview = request.Overview;
- item.Genres = request.Genres;
+ if (request.Studios is not null)
+ {
+ item.Studios = request.Studios.Select(x => x.Name).ToArray();
+ }
- if (item is Episode episode)
- {
- episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
- episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
- episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
- }
+ if (request.DateCreated.HasValue)
+ {
+ item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
+ }
- item.Tags = request.Tags;
+ item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
+ item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
+ item.ProductionYear = request.ProductionYear;
- if (request.Taglines is not null)
- {
- item.Tagline = request.Taglines.FirstOrDefault();
- }
+ request.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
+ item.OfficialRating = request.OfficialRating;
+ item.CustomRating = request.CustomRating;
- if (request.Studios is not null)
+ if (item is Series rseries)
+ {
+ foreach (Season season in rseries.Children)
{
- item.Studios = request.Studios.Select(x => x.Name).ToArray();
- }
+ season.OfficialRating = request.OfficialRating;
+ season.CustomRating = request.CustomRating;
+ season.OnMetadataChanged();
+ await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- if (request.DateCreated.HasValue)
+ foreach (Episode ep in season.Children)
+ {
+ ep.OfficialRating = request.OfficialRating;
+ ep.CustomRating = request.CustomRating;
+ ep.OnMetadataChanged();
+ await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+ }
+ else if (item is Season season)
+ {
+ foreach (Episode ep in season.Children)
{
- item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
+ ep.OfficialRating = request.OfficialRating;
+ ep.CustomRating = request.CustomRating;
+ ep.OnMetadataChanged();
+ await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
-
- item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
- item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
- item.ProductionYear = request.ProductionYear;
- item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
- item.CustomRating = request.CustomRating;
-
- if (request.ProductionLocations is not null)
+ }
+ else if (item is MusicAlbum album)
+ {
+ foreach (BaseItem track in album.Children)
{
- item.ProductionLocations = request.ProductionLocations;
+ track.OfficialRating = request.OfficialRating;
+ track.CustomRating = request.CustomRating;
+ track.OnMetadataChanged();
+ await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
+ }
- item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
- item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
+ if (request.ProductionLocations is not null)
+ {
+ item.ProductionLocations = request.ProductionLocations;
+ }
- if (item is IHasDisplayOrder hasDisplayOrder)
- {
- hasDisplayOrder.DisplayOrder = request.DisplayOrder;
- }
+ item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
+ item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
- if (item is IHasAspectRatio hasAspectRatio)
- {
- hasAspectRatio.AspectRatio = request.AspectRatio;
- }
+ if (item is IHasDisplayOrder hasDisplayOrder)
+ {
+ hasDisplayOrder.DisplayOrder = request.DisplayOrder;
+ }
- item.IsLocked = request.LockData ?? false;
+ if (item is IHasAspectRatio hasAspectRatio)
+ {
+ hasAspectRatio.AspectRatio = request.AspectRatio;
+ }
- if (request.LockedFields is not null)
- {
- item.LockedFields = request.LockedFields;
- }
+ item.IsLocked = request.LockData ?? false;
- // Only allow this for series. Runtimes for media comes from ffprobe.
- if (item is Series)
- {
- item.RunTimeTicks = request.RunTimeTicks;
- }
+ if (request.LockedFields is not null)
+ {
+ item.LockedFields = request.LockedFields;
+ }
- foreach (var pair in request.ProviderIds.ToList())
+ // Only allow this for series. Runtimes for media comes from ffprobe.
+ if (item is Series)
+ {
+ item.RunTimeTicks = request.RunTimeTicks;
+ }
+
+ foreach (var pair in request.ProviderIds.ToList())
+ {
+ if (string.IsNullOrEmpty(pair.Value))
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- request.ProviderIds.Remove(pair.Key);
- }
+ request.ProviderIds.Remove(pair.Key);
}
+ }
- item.ProviderIds = request.ProviderIds;
+ item.ProviderIds = request.ProviderIds;
- if (item is Video video)
- {
- video.Video3DFormat = request.Video3DFormat;
- }
+ if (item is Video video)
+ {
+ video.Video3DFormat = request.Video3DFormat;
+ }
- if (request.AlbumArtists is not null)
+ if (request.AlbumArtists is not null)
+ {
+ if (item is IHasAlbumArtist hasAlbumArtists)
{
- if (item is IHasAlbumArtist hasAlbumArtists)
- {
- hasAlbumArtists.AlbumArtists = request
- .AlbumArtists
- .Select(i => i.Name)
- .ToArray();
- }
+ hasAlbumArtists.AlbumArtists = request
+ .AlbumArtists
+ .Select(i => i.Name)
+ .ToArray();
}
+ }
- if (request.ArtistItems is not null)
+ if (request.ArtistItems is not null)
+ {
+ if (item is IHasArtist hasArtists)
{
- if (item is IHasArtist hasArtists)
- {
- hasArtists.Artists = request
- .ArtistItems
- .Select(i => i.Name)
- .ToArray();
- }
+ hasArtists.Artists = request
+ .ArtistItems
+ .Select(i => i.Name)
+ .ToArray();
}
+ }
- switch (item)
- {
- case Audio song:
- song.Album = request.Album;
- break;
- case MusicVideo musicVideo:
- musicVideo.Album = request.Album;
- break;
- case Series series:
+ switch (item)
+ {
+ case Audio song:
+ song.Album = request.Album;
+ break;
+ case MusicVideo musicVideo:
+ musicVideo.Album = request.Album;
+ break;
+ case Series series:
{
series.Status = GetSeriesStatus(request);
@@ -357,93 +403,92 @@ namespace Jellyfin.Api.Controllers
break;
}
- }
}
+ }
- private SeriesStatus? GetSeriesStatus(BaseItemDto item)
+ private SeriesStatus? GetSeriesStatus(BaseItemDto item)
+ {
+ if (string.IsNullOrEmpty(item.Status))
{
- if (string.IsNullOrEmpty(item.Status))
- {
- return null;
- }
-
- return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
+ return null;
}
- private DateTime NormalizeDateTime(DateTime val)
- {
- return DateTime.SpecifyKind(val, DateTimeKind.Utc);
- }
+ return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
+ }
- private List<NameValuePair> GetContentTypeOptions(bool isForItem)
- {
- var list = new List<NameValuePair>();
+ private DateTime NormalizeDateTime(DateTime val)
+ {
+ return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+ }
- if (isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "Inherit",
- Value = string.Empty
- });
- }
+ private List<NameValuePair> GetContentTypeOptions(bool isForItem)
+ {
+ var list = new List<NameValuePair>();
+ if (isForItem)
+ {
list.Add(new NameValuePair
{
- Name = "Movies",
- Value = "movies"
- });
- list.Add(new NameValuePair
- {
- Name = "Music",
- Value = "music"
- });
- list.Add(new NameValuePair
- {
- Name = "Shows",
- Value = "tvshows"
+ Name = "Inherit",
+ Value = string.Empty
});
+ }
- if (!isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "Books",
- Value = "books"
- });
- }
+ list.Add(new NameValuePair
+ {
+ Name = "Movies",
+ Value = "movies"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "Music",
+ Value = "music"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "Shows",
+ Value = "tvshows"
+ });
+ if (!isForItem)
+ {
list.Add(new NameValuePair
{
- Name = "HomeVideos",
- Value = "homevideos"
- });
- list.Add(new NameValuePair
- {
- Name = "MusicVideos",
- Value = "musicvideos"
- });
- list.Add(new NameValuePair
- {
- Name = "Photos",
- Value = "photos"
+ Name = "Books",
+ Value = "books"
});
+ }
- if (!isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "MixedContent",
- Value = string.Empty
- });
- }
+ list.Add(new NameValuePair
+ {
+ Name = "HomeVideos",
+ Value = "homevideos"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "MusicVideos",
+ Value = "musicvideos"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "Photos",
+ Value = "photos"
+ });
- foreach (var val in list)
+ if (!isForItem)
+ {
+ list.Add(new NameValuePair
{
- val.Name = _localizationManager.GetLocalizedString(val.Name);
- }
+ Name = "MixedContent",
+ Value = string.Empty
+ });
+ }
- return list;
+ foreach (var val in list)
+ {
+ val.Name = _localizationManager.GetLocalizedString(val.Name);
}
+
+ return list;
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 717ddc32b..377526729 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,12 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-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.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -20,854 +19,866 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The items controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class ItemsController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly IDtoService _dtoService;
+ private readonly ILogger<ItemsController> _logger;
+ private readonly ISessionManager _sessionManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemsController"/> class.
+ /// </summary>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ public ItemsController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ IDtoService dtoService,
+ ILogger<ItemsController> logger,
+ ISessionManager sessionManager)
+ {
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _localization = localization;
+ _dtoService = dtoService;
+ _logger = logger;
+ _sessionManager = sessionManager;
+ }
+
/// <summary>
- /// The items controller.
+ /// Gets items based on a query.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ItemsController : BaseJellyfinApiController
+ /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
+ /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+ /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+ /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+ /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+ /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+ /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+ /// <param name="isHd">Optional filter by items that are HD or not.</param>
+ /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+ /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+ /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+ /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+ /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+ /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+ /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+ /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+ /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+ /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
+ /// <param name="isMovie">Optional filter for live tv movies.</param>
+ /// <param name="isSeries">Optional filter for live tv series.</param>
+ /// <param name="isNews">Optional filter for live tv news.</param>
+ /// <param name="isKids">Optional filter for live tv kids.</param>
+ /// <param name="isSports">Optional filter for live tv sports.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+ /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+ /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
+ /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+ /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+ /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
+ /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+ /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+ /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
+ /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
+ /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="isLocked">Optional filter by items that are locked.</param>
+ /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+ /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+ /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+ /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+ /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+ /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+ /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+ /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
+ /// <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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
+ [HttpGet("Items")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetItems(
+ [FromQuery] Guid? userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery] bool? isMissing,
+ [FromQuery] bool? isUnaired,
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] double? minCriticRating,
+ [FromQuery] DateTime? minPremiereDate,
+ [FromQuery] DateTime? minDateLastSaved,
+ [FromQuery] DateTime? minDateLastSavedForUser,
+ [FromQuery] DateTime? maxPremiereDate,
+ [FromQuery] bool? hasOverview,
+ [FromQuery] bool? hasImdbId,
+ [FromQuery] bool? hasTmdbId,
+ [FromQuery] bool? hasTvdbId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery] string? minOfficialRating,
+ [FromQuery] bool? isLocked,
+ [FromQuery] bool? isPlaceHolder,
+ [FromQuery] bool? hasOfficialRating,
+ [FromQuery] bool? collapseBoxSetItems,
+ [FromQuery] int? minWidth,
+ [FromQuery] int? minHeight,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? is3D,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
- private readonly IDtoService _dtoService;
- private readonly ILogger<ItemsController> _logger;
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemsController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- public ItemsController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- IDtoService dtoService,
- ILogger<ItemsController> logger,
- ISessionManager sessionManager)
+ var isApiKey = User.GetIsApiKey();
+ // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = !isApiKey && !userId.Value.Equals(default)
+ ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
+ : null;
+
+ // beyond this point, we're either using an api key or we have a valid user
+ if (!isApiKey && user is null)
+ {
+ return BadRequest("userId is required");
+ }
+
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ if (includeItemTypes.Length == 1
+ && (includeItemTypes[0] == BaseItemKind.Playlist
+ || includeItemTypes[0] == BaseItemKind.BoxSet))
+ {
+ parentId = null;
+ }
+
+ var item = _libraryManager.GetParentItem(parentId, userId);
+ QueryResult<BaseItem> result;
+
+ if (item is not Folder folder)
{
- _userManager = userManager;
- _libraryManager = libraryManager;
- _localization = localization;
- _dtoService = dtoService;
- _logger = logger;
- _sessionManager = sessionManager;
+ folder = _libraryManager.GetUserRootFolder();
}
- /// <summary>
- /// Gets items based on a query.
- /// </summary>
- /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
- /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
- /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
- /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
- /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
- /// <param name="hasTrailer">Optional filter by items with trailers.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
- /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
- /// <param name="isHd">Optional filter by items that are HD or not.</param>
- /// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
- /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
- /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
- /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
- /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
- /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
- /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
- /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
- /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
- /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
- /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
- /// <param name="isMovie">Optional filter for live tv movies.</param>
- /// <param name="isSeries">Optional filter for live tv series.</param>
- /// <param name="isNews">Optional filter for live tv news.</param>
- /// <param name="isKids">Optional filter for live tv kids.</param>
- /// <param name="isSports">Optional filter for live tv sports.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
- /// <param name="searchTerm">Optional. Filter based on a search term.</param>
- /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
- /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
- /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
- /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
- /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
- /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="isLocked">Optional filter by items that are locked.</param>
- /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
- /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
- /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
- /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
- /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
- /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
- /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
- /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
- /// <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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
- [HttpGet("Items")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetItems(
- [FromQuery] Guid? userId,
- [FromQuery] string? maxOfficialRating,
- [FromQuery] bool? hasThemeSong,
- [FromQuery] bool? hasThemeVideo,
- [FromQuery] bool? hasSubtitles,
- [FromQuery] bool? hasSpecialFeature,
- [FromQuery] bool? hasTrailer,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] int? parentIndexNumber,
- [FromQuery] bool? hasParentalRating,
- [FromQuery] bool? isHd,
- [FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
- [FromQuery] bool? isMissing,
- [FromQuery] bool? isUnaired,
- [FromQuery] double? minCommunityRating,
- [FromQuery] double? minCriticRating,
- [FromQuery] DateTime? minPremiereDate,
- [FromQuery] DateTime? minDateLastSaved,
- [FromQuery] DateTime? minDateLastSavedForUser,
- [FromQuery] DateTime? maxPremiereDate,
- [FromQuery] bool? hasOverview,
- [FromQuery] bool? hasImdbId,
- [FromQuery] bool? hasTmdbId,
- [FromQuery] bool? hasTvdbId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? recursive,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
- [FromQuery] string? minOfficialRating,
- [FromQuery] bool? isLocked,
- [FromQuery] bool? isPlaceHolder,
- [FromQuery] bool? hasOfficialRating,
- [FromQuery] bool? collapseBoxSetItems,
- [FromQuery] int? minWidth,
- [FromQuery] int? minHeight,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true)
+ string? collectionType = null;
+ if (folder is IHasCollectionType hasCollectionType)
{
- var isApiKey = User.GetIsApiKey();
- // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
- var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default)
- ? _userManager.GetUserById(userId.Value)
- : null;
-
- // beyond this point, we're either using an api key or we have a valid user
- if (!isApiKey && user is null)
+ collectionType = hasCollectionType.CollectionType;
+ }
+
+ if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ {
+ recursive = true;
+ includeItemTypes = new[] { BaseItemKind.Playlist };
+ }
+
+ if (item is not UserRootFolder
+ // api keys can always access all folders
+ && !isApiKey
+ // check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ _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}.");
+ }
+
+ if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
+ {
+ var query = new InternalItemsQuery(user)
{
- return BadRequest("userId is required");
- }
+ IsPlayed = isPlayed,
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ Recursive = recursive ?? false,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+ IsFavorite = isFavorite,
+ Limit = limit,
+ StartIndex = startIndex,
+ IsMissing = isMissing,
+ IsUnaired = isUnaired,
+ CollapseBoxSetItems = collapseBoxSetItems,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ HasImdbId = hasImdbId,
+ IsPlaceHolder = isPlaceHolder,
+ IsLocked = isLocked,
+ MinWidth = minWidth,
+ MinHeight = minHeight,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ Is3D = is3D,
+ HasTvdbId = hasTvdbId,
+ HasTmdbId = hasTmdbId,
+ IsMovie = isMovie,
+ IsSeries = isSeries,
+ IsNews = isNews,
+ IsKids = isKids,
+ IsSports = isSports,
+ HasOverview = hasOverview,
+ HasOfficialRating = hasOfficialRating,
+ HasParentalRating = hasParentalRating,
+ HasSpecialFeature = hasSpecialFeature,
+ HasSubtitles = hasSubtitles,
+ HasThemeSong = hasThemeSong,
+ HasThemeVideo = hasThemeVideo,
+ HasTrailer = hasTrailer,
+ IsHD = isHd,
+ Is4K = is4K,
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ ArtistIds = artistIds,
+ AlbumArtistIds = albumArtistIds,
+ ContributingArtistIds = contributingArtistIds,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
+ Person = person,
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
+ ImageTypes = imageTypes,
+ VideoTypes = videoTypes,
+ AdjacentTo = adjacentTo,
+ ItemIds = ids,
+ MinCommunityRating = minCommunityRating,
+ MinCriticRating = minCriticRating,
+ ParentId = parentId ?? Guid.Empty,
+ ParentIndexNumber = parentIndexNumber,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ ExcludeItemIds = excludeItemIds,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
+ MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
+ MinPremiereDate = minPremiereDate?.ToUniversalTime(),
+ MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
+ };
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
+ {
+ query.CollapseBoxSetItems = false;
+ }
- if (includeItemTypes.Length == 1
- && (includeItemTypes[0] == BaseItemKind.Playlist
- || includeItemTypes[0] == BaseItemKind.BoxSet))
+ foreach (var filter in filters)
{
- parentId = null;
+ switch (filter)
+ {
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
+ }
}
- var item = _libraryManager.GetParentItem(parentId, userId);
- QueryResult<BaseItem> result;
+ // Filter by Series Status
+ if (seriesStatus.Length != 0)
+ {
+ query.SeriesStatuses = seriesStatus;
+ }
- if (item is not Folder folder)
+ // Exclude Blocked Unrated Items
+ var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems);
+ if (blockedUnratedItems is not null)
{
- folder = _libraryManager.GetUserRootFolder();
+ query.BlockUnratedItems = blockedUnratedItems;
}
- string? collectionType = null;
- if (folder is IHasCollectionType hasCollectionType)
+ // ExcludeLocationTypes
+ if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{
- collectionType = hasCollectionType.CollectionType;
+ query.IsVirtualItem = false;
}
- if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ if (locationTypes.Length > 0 && locationTypes.Length < 4)
{
- recursive = true;
- includeItemTypes = new[] { BaseItemKind.Playlist };
+ query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
}
- if (item is not UserRootFolder
- // api keys can always access all folders
- && !isApiKey
- // check the item is visible for the user
- && !item.IsVisible(user))
+ // Min official rating
+ if (!string.IsNullOrWhiteSpace(minOfficialRating))
{
- _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}.");
+ query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
}
- if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
+ // Max official rating
+ if (!string.IsNullOrWhiteSpace(maxOfficialRating))
{
- var query = new InternalItemsQuery(user)
- {
- IsPlayed = isPlayed,
- MediaTypes = mediaTypes,
- IncludeItemTypes = includeItemTypes,
- ExcludeItemTypes = excludeItemTypes,
- Recursive = recursive ?? false,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
- IsFavorite = isFavorite,
- Limit = limit,
- StartIndex = startIndex,
- IsMissing = isMissing,
- IsUnaired = isUnaired,
- CollapseBoxSetItems = collapseBoxSetItems,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- HasImdbId = hasImdbId,
- IsPlaceHolder = isPlaceHolder,
- IsLocked = isLocked,
- MinWidth = minWidth,
- MinHeight = minHeight,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- Is3D = is3D,
- HasTvdbId = hasTvdbId,
- HasTmdbId = hasTmdbId,
- IsMovie = isMovie,
- IsSeries = isSeries,
- IsNews = isNews,
- IsKids = isKids,
- IsSports = isSports,
- HasOverview = hasOverview,
- HasOfficialRating = hasOfficialRating,
- HasParentalRating = hasParentalRating,
- HasSpecialFeature = hasSpecialFeature,
- HasSubtitles = hasSubtitles,
- HasThemeSong = hasThemeSong,
- HasThemeVideo = hasThemeVideo,
- HasTrailer = hasTrailer,
- IsHD = isHd,
- Is4K = is4K,
- Tags = tags,
- OfficialRatings = officialRatings,
- Genres = genres,
- ArtistIds = artistIds,
- AlbumArtistIds = albumArtistIds,
- ContributingArtistIds = contributingArtistIds,
- GenreIds = genreIds,
- StudioIds = studioIds,
- Person = person,
- PersonIds = personIds,
- PersonTypes = personTypes,
- Years = years,
- ImageTypes = imageTypes,
- VideoTypes = videoTypes,
- AdjacentTo = adjacentTo,
- ItemIds = ids,
- MinCommunityRating = minCommunityRating,
- MinCriticRating = minCriticRating,
- ParentId = parentId ?? Guid.Empty,
- ParentIndexNumber = parentIndexNumber,
- EnableTotalRecordCount = enableTotalRecordCount,
- ExcludeItemIds = excludeItemIds,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
- MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
- MinPremiereDate = minPremiereDate?.ToUniversalTime(),
- MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
- };
-
- if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
- {
- query.CollapseBoxSetItems = false;
- }
+ query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
+ }
- foreach (var filter in filters)
+ // Artists
+ if (artists.Length != 0)
+ {
+ query.ArtistIds = artists.Select(i =>
{
- switch (filter)
+ try
{
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
+ return _libraryManager.GetArtist(i, new DtoOptions(false));
}
- }
-
- // Filter by Series Status
- if (seriesStatus.Length != 0)
- {
- query.SeriesStatuses = seriesStatus;
- }
-
- // ExcludeLocationTypes
- if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
- {
- query.IsVirtualItem = false;
- }
-
- if (locationTypes.Length > 0 && locationTypes.Length < 4)
- {
- query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
- }
-
- // Min official rating
- if (!string.IsNullOrWhiteSpace(minOfficialRating))
- {
- query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
- }
-
- // Max official rating
- if (!string.IsNullOrWhiteSpace(maxOfficialRating))
- {
- query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
- }
-
- // Artists
- if (artists.Length != 0)
- {
- query.ArtistIds = artists.Select(i =>
+ catch
{
- try
- {
- return _libraryManager.GetArtist(i, new DtoOptions(false));
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
- }
+ return null;
+ }
+ }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ }
- // ExcludeArtistIds
- if (excludeArtistIds.Length != 0)
- {
- query.ExcludeArtistIds = excludeArtistIds;
- }
+ // ExcludeArtistIds
+ if (excludeArtistIds.Length != 0)
+ {
+ query.ExcludeArtistIds = excludeArtistIds;
+ }
- if (albumIds.Length != 0)
- {
- query.AlbumIds = albumIds;
- }
+ if (albumIds.Length != 0)
+ {
+ query.AlbumIds = albumIds;
+ }
- // Albums
- if (albums.Length != 0)
+ // Albums
+ if (albums.Length != 0)
+ {
+ query.AlbumIds = albums.SelectMany(i =>
{
- query.AlbumIds = albums.SelectMany(i =>
- {
- return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 });
- }).ToArray();
- }
+ return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 });
+ }).ToArray();
+ }
- // Studios
- if (studios.Length != 0)
+ // Studios
+ if (studios.Length != 0)
+ {
+ query.StudioIds = studios.Select(i =>
{
- query.StudioIds = studios.Select(i =>
+ try
{
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
- }
-
- // Apply default sorting if none requested
- if (query.OrderBy.Count == 0)
- {
- // Albums by artist
- if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
+ return _libraryManager.GetStudio(i);
+ }
+ catch
{
- query.OrderBy = new[] { (ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending) };
+ return null;
}
- }
-
- result = folder.GetItems(query);
+ }).Where(i => i is not null).Select(i => i!.Id).ToArray();
}
- else
+
+ // Apply default sorting if none requested
+ if (query.OrderBy.Count == 0)
{
- var itemsArray = folder.GetChildren(user, true);
- result = new QueryResult<BaseItem>(itemsArray);
+ // Albums by artist
+ if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
+ {
+ query.OrderBy = new[] { (ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending) };
+ }
}
- return new QueryResult<BaseItemDto>(
- startIndex,
- result.TotalRecordCount,
- _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user));
+ result = folder.GetItems(query);
}
-
- /// <summary>
- /// Gets items based on a query.
- /// </summary>
- /// <param name="userId">The user id supplied as query parameter.</param>
- /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
- /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
- /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
- /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
- /// <param name="hasTrailer">Optional filter by items with trailers.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
- /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
- /// <param name="isHd">Optional filter by items that are HD or not.</param>
- /// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
- /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
- /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
- /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
- /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
- /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
- /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
- /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
- /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
- /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
- /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
- /// <param name="isMovie">Optional filter for live tv movies.</param>
- /// <param name="isSeries">Optional filter for live tv series.</param>
- /// <param name="isNews">Optional filter for live tv news.</param>
- /// <param name="isKids">Optional filter for live tv kids.</param>
- /// <param name="isSports">Optional filter for live tv sports.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
- /// <param name="searchTerm">Optional. Filter based on a search term.</param>
- /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
- /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
- /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
- /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
- /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
- /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="isLocked">Optional filter by items that are locked.</param>
- /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
- /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
- /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
- /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
- /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
- /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
- /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
- /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
- /// <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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
- [HttpGet("Users/{userId}/Items")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
- [FromRoute] Guid userId,
- [FromQuery] string? maxOfficialRating,
- [FromQuery] bool? hasThemeSong,
- [FromQuery] bool? hasThemeVideo,
- [FromQuery] bool? hasSubtitles,
- [FromQuery] bool? hasSpecialFeature,
- [FromQuery] bool? hasTrailer,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] int? parentIndexNumber,
- [FromQuery] bool? hasParentalRating,
- [FromQuery] bool? isHd,
- [FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
- [FromQuery] bool? isMissing,
- [FromQuery] bool? isUnaired,
- [FromQuery] double? minCommunityRating,
- [FromQuery] double? minCriticRating,
- [FromQuery] DateTime? minPremiereDate,
- [FromQuery] DateTime? minDateLastSaved,
- [FromQuery] DateTime? minDateLastSavedForUser,
- [FromQuery] DateTime? maxPremiereDate,
- [FromQuery] bool? hasOverview,
- [FromQuery] bool? hasImdbId,
- [FromQuery] bool? hasTmdbId,
- [FromQuery] bool? hasTvdbId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? recursive,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
- [FromQuery] string? minOfficialRating,
- [FromQuery] bool? isLocked,
- [FromQuery] bool? isPlaceHolder,
- [FromQuery] bool? hasOfficialRating,
- [FromQuery] bool? collapseBoxSetItems,
- [FromQuery] int? minWidth,
- [FromQuery] int? minHeight,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true)
+ else
{
- return GetItems(
- userId,
- maxOfficialRating,
- hasThemeSong,
- hasThemeVideo,
- hasSubtitles,
- hasSpecialFeature,
- hasTrailer,
- adjacentTo,
- parentIndexNumber,
- hasParentalRating,
- isHd,
- is4K,
- locationTypes,
- excludeLocationTypes,
- isMissing,
- isUnaired,
- minCommunityRating,
- minCriticRating,
- minPremiereDate,
- minDateLastSaved,
- minDateLastSavedForUser,
- maxPremiereDate,
- hasOverview,
- hasImdbId,
- hasTmdbId,
- hasTvdbId,
- isMovie,
- isSeries,
- isNews,
- isKids,
- isSports,
- excludeItemIds,
- startIndex,
- limit,
- recursive,
- searchTerm,
- sortOrder,
- parentId,
- fields,
- excludeItemTypes,
- includeItemTypes,
- filters,
- isFavorite,
- mediaTypes,
- imageTypes,
- sortBy,
- isPlayed,
- genres,
- officialRatings,
- tags,
- years,
- enableUserData,
- imageTypeLimit,
- enableImageTypes,
- person,
- personIds,
- personTypes,
- studios,
- artists,
- excludeArtistIds,
- artistIds,
- albumArtistIds,
- contributingArtistIds,
- albums,
- albumIds,
- ids,
- videoTypes,
- minOfficialRating,
- isLocked,
- isPlaceHolder,
- hasOfficialRating,
- collapseBoxSetItems,
- minWidth,
- minHeight,
- maxWidth,
- maxHeight,
- is3D,
- seriesStatus,
- nameStartsWithOrGreater,
- nameStartsWith,
- nameLessThan,
- studioIds,
- genreIds,
- enableTotalRecordCount,
- enableImages);
+ var itemsArray = folder.GetChildren(user, true);
+ result = new QueryResult<BaseItem>(itemsArray);
}
- /// <summary>
- /// Gets items based on a query.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="startIndex">The start index.</param>
- /// <param name="limit">The item limit.</param>
- /// <param name="searchTerm">The search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</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>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
- /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
- /// <response code="200">Items returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
- [HttpGet("Users/{userId}/Items/Resume")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
- [FromRoute, Required] Guid userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool excludeActiveSessions = false)
- {
- var user = _userManager.GetUserById(userId);
- var parentIdGuid = parentId ?? Guid.Empty;
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ result.TotalRecordCount,
+ _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user));
+ }
- var ancestorIds = Array.Empty<Guid>();
+ /// <summary>
+ /// Gets items based on a query.
+ /// </summary>
+ /// <param name="userId">The user id supplied as query parameter.</param>
+ /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+ /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+ /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+ /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+ /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+ /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+ /// <param name="isHd">Optional filter by items that are HD or not.</param>
+ /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+ /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+ /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+ /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+ /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+ /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+ /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+ /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+ /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+ /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
+ /// <param name="isMovie">Optional filter for live tv movies.</param>
+ /// <param name="isSeries">Optional filter for live tv series.</param>
+ /// <param name="isNews">Optional filter for live tv news.</param>
+ /// <param name="isKids">Optional filter for live tv kids.</param>
+ /// <param name="isSports">Optional filter for live tv sports.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+ /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+ /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
+ /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+ /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+ /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
+ /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+ /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+ /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
+ /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
+ /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="isLocked">Optional filter by items that are locked.</param>
+ /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+ /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+ /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+ /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+ /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+ /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+ /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+ /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
+ /// <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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
+ [HttpGet("Users/{userId}/Items")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
+ [FromRoute] Guid userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery] bool? isMissing,
+ [FromQuery] bool? isUnaired,
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] double? minCriticRating,
+ [FromQuery] DateTime? minPremiereDate,
+ [FromQuery] DateTime? minDateLastSaved,
+ [FromQuery] DateTime? minDateLastSavedForUser,
+ [FromQuery] DateTime? maxPremiereDate,
+ [FromQuery] bool? hasOverview,
+ [FromQuery] bool? hasImdbId,
+ [FromQuery] bool? hasTmdbId,
+ [FromQuery] bool? hasTvdbId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery] string? minOfficialRating,
+ [FromQuery] bool? isLocked,
+ [FromQuery] bool? isPlaceHolder,
+ [FromQuery] bool? hasOfficialRating,
+ [FromQuery] bool? collapseBoxSetItems,
+ [FromQuery] int? minWidth,
+ [FromQuery] int? minHeight,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? is3D,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ return GetItems(
+ userId,
+ maxOfficialRating,
+ hasThemeSong,
+ hasThemeVideo,
+ hasSubtitles,
+ hasSpecialFeature,
+ hasTrailer,
+ adjacentTo,
+ parentIndexNumber,
+ hasParentalRating,
+ isHd,
+ is4K,
+ locationTypes,
+ excludeLocationTypes,
+ isMissing,
+ isUnaired,
+ minCommunityRating,
+ minCriticRating,
+ minPremiereDate,
+ minDateLastSaved,
+ minDateLastSavedForUser,
+ maxPremiereDate,
+ hasOverview,
+ hasImdbId,
+ hasTmdbId,
+ hasTvdbId,
+ isMovie,
+ isSeries,
+ isNews,
+ isKids,
+ isSports,
+ excludeItemIds,
+ startIndex,
+ limit,
+ recursive,
+ searchTerm,
+ sortOrder,
+ parentId,
+ fields,
+ excludeItemTypes,
+ includeItemTypes,
+ filters,
+ isFavorite,
+ mediaTypes,
+ imageTypes,
+ sortBy,
+ isPlayed,
+ genres,
+ officialRatings,
+ tags,
+ years,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes,
+ person,
+ personIds,
+ personTypes,
+ studios,
+ artists,
+ excludeArtistIds,
+ artistIds,
+ albumArtistIds,
+ contributingArtistIds,
+ albums,
+ albumIds,
+ ids,
+ videoTypes,
+ minOfficialRating,
+ isLocked,
+ isPlaceHolder,
+ hasOfficialRating,
+ collapseBoxSetItems,
+ minWidth,
+ minHeight,
+ maxWidth,
+ maxHeight,
+ is3D,
+ seriesStatus,
+ nameStartsWithOrGreater,
+ nameStartsWith,
+ nameLessThan,
+ studioIds,
+ genreIds,
+ enableTotalRecordCount,
+ enableImages);
+ }
- var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
- if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0)
- {
- ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
- .Where(i => i is Folder)
- .Where(i => !excludeFolderIds.Contains(i.Id))
- .Select(i => i.Id)
- .ToArray();
- }
+ /// <summary>
+ /// Gets items based on a query.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The item limit.</param>
+ /// <param name="searchTerm">The search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</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>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
+ /// <response code="200">Items returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
+ [HttpGet("Users/{userId}/Items/Resume")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
+ [FromRoute, Required] Guid userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool excludeActiveSessions = false)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- var excludeItemIds = Array.Empty<Guid>();
- if (excludeActiveSessions)
- {
- excludeItemIds = _sessionManager.Sessions
- .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem is not null)
- .Select(s => s.NowPlayingItem.Id)
- .ToArray();
- }
+ var parentIdGuid = parentId ?? Guid.Empty;
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
- {
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
- IsResumable = true,
- StartIndex = startIndex,
- Limit = limit,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = dtoOptions,
- MediaTypes = mediaTypes,
- IsVirtualItem = false,
- CollapseBoxSetItems = false,
- EnableTotalRecordCount = enableTotalRecordCount,
- AncestorIds = ancestorIds,
- IncludeItemTypes = includeItemTypes,
- ExcludeItemTypes = excludeItemTypes,
- SearchTerm = searchTerm,
- ExcludeItemIds = excludeItemIds
- });
+ var ancestorIds = Array.Empty<Guid>();
- var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
+ var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
+ if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0)
+ {
+ ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ .Where(i => i is Folder)
+ .Where(i => !excludeFolderIds.Contains(i.Id))
+ .Select(i => i.Id)
+ .ToArray();
+ }
- return new QueryResult<BaseItemDto>(
- startIndex,
- itemsResult.TotalRecordCount,
- returnItems);
+ var excludeItemIds = Array.Empty<Guid>();
+ if (excludeActiveSessions)
+ {
+ excludeItemIds = _sessionManager.Sessions
+ .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem is not null)
+ .Select(s => s.NowPlayingItem.Id)
+ .ToArray();
}
+
+ var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
+ {
+ OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
+ IsResumable = true,
+ StartIndex = startIndex,
+ Limit = limit,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ DtoOptions = dtoOptions,
+ MediaTypes = mediaTypes,
+ IsVirtualItem = false,
+ CollapseBoxSetItems = false,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ AncestorIds = ancestorIds,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ SearchTerm = searchTerm,
+ ExcludeItemIds = excludeItemIds
+ });
+
+ var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
+
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ itemsResult.TotalRecordCount,
+ returnItems);
}
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 196d509fb..bf59febed 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -4,13 +4,12 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Net;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.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;
@@ -37,773 +36,792 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Library Controller.
+/// </summary>
+[Route("")]
+public class LibraryController : BaseJellyfinApiController
{
+ private readonly IProviderManager _providerManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+ private readonly IActivityManager _activityManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILogger<LibraryController> _logger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Library Controller.
+ /// Initializes a new instance of the <see cref="LibraryController"/> class.
/// </summary>
- [Route("")]
- public class LibraryController : BaseJellyfinApiController
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{LibraryController}"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public LibraryController(
+ IProviderManager providerManager,
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService,
+ IActivityManager activityManager,
+ ILocalizationManager localization,
+ ILibraryMonitor libraryMonitor,
+ ILogger<LibraryController> logger,
+ IServerConfigurationManager serverConfigurationManager)
{
- private readonly IProviderManager _providerManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly IActivityManager _activityManager;
- private readonly ILocalizationManager _localization;
- private readonly ILibraryMonitor _libraryMonitor;
- private readonly ILogger<LibraryController> _logger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryController"/> class.
- /// </summary>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{LibraryController}"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public LibraryController(
- IProviderManager providerManager,
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService,
- IActivityManager activityManager,
- ILocalizationManager localization,
- ILibraryMonitor libraryMonitor,
- ILogger<LibraryController> logger,
- IServerConfigurationManager serverConfigurationManager)
- {
- _providerManager = providerManager;
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- _activityManager = activityManager;
- _localization = localization;
- _libraryMonitor = libraryMonitor;
- _logger = logger;
- _serverConfigurationManager = serverConfigurationManager;
- }
-
- /// <summary>
- /// Get the original file of an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="200">File stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
- [HttpGet("Items/{itemId}/File")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesFile("video/*", "audio/*")]
- public ActionResult GetFile([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ _providerManager = providerManager;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _activityManager = activityManager;
+ _localization = localization;
+ _libraryMonitor = libraryMonitor;
+ _logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
+ /// <summary>
+ /// Get the original file of an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="200">File stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
+ [HttpGet("Items/{itemId}/File")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile("video/*", "audio/*")]
+ public ActionResult GetFile([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets critic review for an item.
- /// </summary>
- /// <response code="200">Critic reviews returned.</response>
- /// <returns>The list of critic reviews.</returns>
- [HttpGet("Items/{itemId}/CriticReviews")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
+ }
+
+ /// <summary>
+ /// Gets critic review for an item.
+ /// </summary>
+ /// <response code="200">Critic reviews returned.</response>
+ /// <returns>The list of critic reviews.</returns>
+ [HttpGet("Items/{itemId}/CriticReviews")]
+ [Authorize]
+ [Obsolete("This endpoint is obsolete.")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
+ {
+ return new QueryResult<BaseItemDto>();
+ }
+
+ /// <summary>
+ /// Get theme songs for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <response code="200">Theme songs returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The item theme songs.</returns>
+ [HttpGet("Items/{itemId}/ThemeSongs")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<ThemeMediaResult> GetThemeSongs(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var item = itemId.Equals(default)
+ ? (userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
{
- return new QueryResult<BaseItemDto>();
+ return NotFound("Item not found.");
}
- /// <summary>
- /// Get theme songs for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
- /// <response code="200">Theme songs returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The item theme songs.</returns>
- [HttpGet("Items/{itemId}/ThemeSongs")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<ThemeMediaResult> GetThemeSongs(
- [FromRoute, Required] Guid itemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
- if (item is null)
+ IEnumerable<BaseItem> themeItems;
+
+ while (true)
+ {
+ themeItems = item.GetThemeSongs();
+
+ if (themeItems.Any() || !inheritFromParent)
{
- return NotFound("Item not found.");
+ break;
}
- IEnumerable<BaseItem> themeItems;
-
- while (true)
+ var parent = item.GetParent();
+ if (parent is null)
{
- themeItems = item.GetThemeSongs();
+ break;
+ }
- if (themeItems.Any() || !inheritFromParent)
- {
- break;
- }
+ item = parent;
+ }
- var parent = item.GetParent();
- if (parent is null)
- {
- break;
- }
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var items = themeItems
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+ .ToArray();
- item = parent;
- }
+ return new ThemeMediaResult
+ {
+ Items = items,
+ TotalRecordCount = items.Length,
+ OwnerId = item.Id
+ };
+ }
+
+ /// <summary>
+ /// Get theme videos for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <response code="200">Theme videos returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The item theme videos.</returns>
+ [HttpGet("Items/{itemId}/ThemeVideos")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<ThemeMediaResult> GetThemeVideos(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var item = itemId.Equals(default)
+ ? (userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
+ {
+ return NotFound("Item not found.");
+ }
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var items = themeItems
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
+ IEnumerable<BaseItem> themeItems;
+
+ while (true)
+ {
+ themeItems = item.GetThemeVideos();
- return new ThemeMediaResult
+ if (themeItems.Any() || !inheritFromParent)
{
- Items = items,
- TotalRecordCount = items.Length,
- OwnerId = item.Id
- };
- }
-
- /// <summary>
- /// Get theme videos for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
- /// <response code="200">Theme videos returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The item theme videos.</returns>
- [HttpGet("Items/{itemId}/ThemeVideos")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<ThemeMediaResult> GetThemeVideos(
- [FromRoute, Required] Guid itemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
- if (item is null)
+ break;
+ }
+
+ var parent = item.GetParent();
+ if (parent is null)
{
- return NotFound("Item not found.");
+ break;
}
- IEnumerable<BaseItem> themeItems;
+ item = parent;
+ }
- while (true)
- {
- themeItems = item.GetThemeVideos();
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var items = themeItems
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+ .ToArray();
- if (themeItems.Any() || !inheritFromParent)
- {
- break;
- }
+ return new ThemeMediaResult
+ {
+ Items = items,
+ TotalRecordCount = items.Length,
+ OwnerId = item.Id
+ };
+ }
- var parent = item.GetParent();
- if (parent is null)
- {
- break;
- }
+ /// <summary>
+ /// Get theme songs and videos for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <response code="200">Theme songs and videos returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The item theme videos.</returns>
+ [HttpGet("Items/{itemId}/ThemeMedia")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<AllThemeMediaResult> GetThemeMedia(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ var themeSongs = GetThemeSongs(
+ itemId,
+ userId,
+ inheritFromParent);
- item = parent;
- }
+ var themeVideos = GetThemeVideos(
+ itemId,
+ userId,
+ inheritFromParent);
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var items = themeItems
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
+ if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
+ {
+ return NotFound();
+ }
- return new ThemeMediaResult
- {
- Items = items,
- TotalRecordCount = items.Length,
- OwnerId = item.Id
- };
- }
-
- /// <summary>
- /// Get theme songs and videos for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
- /// <response code="200">Theme songs and videos returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The item theme videos.</returns>
- [HttpGet("Items/{itemId}/ThemeMedia")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<AllThemeMediaResult> GetThemeMedia(
- [FromRoute, Required] Guid itemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
- {
- var themeSongs = GetThemeSongs(
- itemId,
- userId,
- inheritFromParent);
-
- var themeVideos = GetThemeVideos(
- itemId,
- userId,
- inheritFromParent);
-
- return new AllThemeMediaResult
- {
- ThemeSongsResult = themeSongs?.Value,
- ThemeVideosResult = themeVideos?.Value,
- SoundtrackSongsResult = new ThemeMediaResult()
- };
- }
-
- /// <summary>
- /// Starts a library scan.
- /// </summary>
- /// <response code="204">Library scan started.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Library/Refresh")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RefreshLibrary()
- {
- try
- {
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing library");
- }
+ return new AllThemeMediaResult
+ {
+ ThemeSongsResult = themeSongs?.Value,
+ ThemeVideosResult = themeVideos?.Value,
+ SoundtrackSongsResult = new ThemeMediaResult()
+ };
+ }
+
+ /// <summary>
+ /// Starts a library scan.
+ /// </summary>
+ /// <response code="204">Library scan started.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Refresh")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RefreshLibrary()
+ {
+ try
+ {
+ await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing library");
+ }
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Deletes an item from the library and filesystem.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="204">Item deleted.</response>
+ /// <response code="401">Unauthorized access.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Items/{itemId}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public ActionResult DeleteItem(Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ var user = _userManager.GetUserById(User.GetUserId());
+
+ if (!item.CanDelete(user))
+ {
+ return Unauthorized("Unauthorized access");
+ }
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions { DeleteFileLocation = true },
+ true);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Deletes items from the library and filesystem.
+ /// </summary>
+ /// <param name="ids">The item ids.</param>
+ /// <response code="204">Items deleted.</response>
+ /// <response code="401">Unauthorized access.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Items")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ if (ids.Length == 0)
+ {
return NoContent();
}
- /// <summary>
- /// Deletes an item from the library and filesystem.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="204">Item deleted.</response>
- /// <response code="401">Unauthorized access.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Items/{itemId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItem(Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
+ foreach (var i in ids)
+ {
+ var item = _libraryManager.GetItemById(i);
var user = _userManager.GetUserById(User.GetUserId());
if (!item.CanDelete(user))
{
- return Unauthorized("Unauthorized access");
+ if (ids.Length > 1)
+ {
+ return Unauthorized("Unauthorized access");
+ }
+
+ continue;
}
_libraryManager.DeleteItem(
item,
new DeleteOptions { DeleteFileLocation = true },
true);
-
- return NoContent();
}
- /// <summary>
- /// Deletes items from the library and filesystem.
- /// </summary>
- /// <param name="ids">The item ids.</param>
- /// <response code="204">Items deleted.</response>
- /// <response code="401">Unauthorized access.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Items")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
- {
- if (ids.Length == 0)
- {
- return NoContent();
- }
-
- foreach (var i in ids)
- {
- var item = _libraryManager.GetItemById(i);
- var user = _userManager.GetUserById(User.GetUserId());
+ return NoContent();
+ }
- if (!item.CanDelete(user))
- {
- if (ids.Length > 1)
- {
- return Unauthorized("Unauthorized access");
- }
+ /// <summary>
+ /// Get item counts.
+ /// </summary>
+ /// <param name="userId">Optional. Get counts from a specific user's library.</param>
+ /// <param name="isFavorite">Optional. Get counts of favorite items.</param>
+ /// <response code="200">Item counts returned.</response>
+ /// <returns>Item counts.</returns>
+ [HttpGet("Items/Counts")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<ItemCounts> GetItemCounts(
+ [FromQuery] Guid? userId,
+ [FromQuery] bool? isFavorite)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- continue;
- }
+ var counts = new ItemCounts
+ {
+ AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
+ EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
+ MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
+ SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
+ SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
+ MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
+ BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
+ BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
+ };
+
+ return counts;
+ }
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions { DeleteFileLocation = true },
- true);
- }
+ /// <summary>
+ /// Gets all parents of an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Item parents returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>Item parents.</returns>
+ [HttpGet("Items/{itemId}/Ancestors")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ userId = RequestHelpers.GetUserId(User, userId);
- return NoContent();
+ if (item is null)
+ {
+ return NotFound("Item not found");
}
- /// <summary>
- /// Get item counts.
- /// </summary>
- /// <param name="userId">Optional. Get counts from a specific user's library.</param>
- /// <param name="isFavorite">Optional. Get counts of favorite items.</param>
- /// <response code="200">Item counts returned.</response>
- /// <returns>Item counts.</returns>
- [HttpGet("Items/Counts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<ItemCounts> GetItemCounts(
- [FromQuery] Guid? userId,
- [FromQuery] bool? isFavorite)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var counts = new ItemCounts
- {
- AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
- EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
- MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
- SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
- SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
- MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
- BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
- BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
- };
-
- return counts;
- }
-
- /// <summary>
- /// Gets all parents of an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Item parents returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>Item parents.</returns>
- [HttpGet("Items/{itemId}/Ancestors")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
- {
- var item = _libraryManager.GetItemById(itemId);
-
- if (item is null)
- {
- return NotFound("Item not found");
- }
-
- var baseItemDtos = new List<BaseItemDto>();
+ var baseItemDtos = new List<BaseItemDto>();
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions().AddClientFields(User);
- BaseItem? parent = item.GetParent();
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ BaseItem? parent = item.GetParent();
- while (parent is not null)
+ while (parent is not null)
+ {
+ if (user is not null)
{
- if (user is not null)
+ parent = TranslateParentItem(parent, user);
+ if (parent is null)
{
- parent = TranslateParentItem(parent, user);
+ break;
}
-
- baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
-
- parent = parent?.GetParent();
}
- return baseItemDtos;
- }
+ baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
- /// <summary>
- /// Gets a list of physical paths from virtual folders.
- /// </summary>
- /// <response code="200">Physical paths returned.</response>
- /// <returns>List of physical paths.</returns>
- [HttpGet("Library/PhysicalPaths")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<string>> GetPhysicalPaths()
- {
- return Ok(_libraryManager.RootFolder.Children
- .SelectMany(c => c.PhysicalLocations));
+ parent = parent?.GetParent();
}
- /// <summary>
- /// Gets all user media folders.
- /// </summary>
- /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
- /// <response code="200">Media folders returned.</response>
- /// <returns>List of user media folders.</returns>
- [HttpGet("Library/MediaFolders")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
- {
- var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
+ return baseItemDtos;
+ }
- if (isHidden.HasValue)
- {
- var val = isHidden.Value;
+ /// <summary>
+ /// Gets a list of physical paths from virtual folders.
+ /// </summary>
+ /// <response code="200">Physical paths returned.</response>
+ /// <returns>List of physical paths.</returns>
+ [HttpGet("Library/PhysicalPaths")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<string>> GetPhysicalPaths()
+ {
+ return Ok(_libraryManager.RootFolder.Children
+ .SelectMany(c => c.PhysicalLocations));
+ }
- items = items.Where(i => i.IsHidden == val).ToList();
- }
+ /// <summary>
+ /// Gets all user media folders.
+ /// </summary>
+ /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
+ /// <response code="200">Media folders returned.</response>
+ /// <returns>List of user media folders.</returns>
+ [HttpGet("Library/MediaFolders")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
+ {
+ var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
+
+ if (isHidden.HasValue)
+ {
+ var val = isHidden.Value;
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
- return new QueryResult<BaseItemDto>(resultArray);
+ items = items.Where(i => i.IsHidden == val).ToList();
}
- /// <summary>
- /// Reports that new episodes of a series have been added by an external source.
- /// </summary>
- /// <param name="tvdbId">The tvdbId.</param>
- /// <response code="204">Report success.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
- [HttpPost("Library/Series/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
- {
- var series = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { BaseItemKind.Series },
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
+ return new QueryResult<BaseItemDto>(resultArray);
+ }
- foreach (var item in series)
+ /// <summary>
+ /// Reports that new episodes of a series have been added by an external source.
+ /// </summary>
+ /// <param name="tvdbId">The tvdbId.</param>
+ /// <response code="204">Report success.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
+ [HttpPost("Library/Series/Updated")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
+ {
+ var series = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { BaseItemKind.Series },
+ DtoOptions = new DtoOptions(false)
{
- _libraryMonitor.ReportFileSystemChanged(item.Path);
+ EnableImages = false
}
+ }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
- return NoContent();
+ foreach (var item in series)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path);
}
- /// <summary>
- /// Reports that new movies have been added by an external source.
- /// </summary>
- /// <param name="tmdbId">The tmdbId.</param>
- /// <param name="imdbId">The imdbId.</param>
- /// <response code="204">Report success.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
- [HttpPost("Library/Movies/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
- {
- var movies = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { BaseItemKind.Movie },
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- });
-
- if (!string.IsNullOrWhiteSpace(imdbId))
- {
- movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
- }
- else if (!string.IsNullOrWhiteSpace(tmdbId))
- {
- movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
- }
- else
- {
- movies = new List<BaseItem>();
- }
+ return NoContent();
+ }
- foreach (var item in movies)
+ /// <summary>
+ /// Reports that new movies have been added by an external source.
+ /// </summary>
+ /// <param name="tmdbId">The tmdbId.</param>
+ /// <param name="imdbId">The imdbId.</param>
+ /// <response code="204">Report success.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
+ [HttpPost("Library/Movies/Updated")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
+ {
+ var movies = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { BaseItemKind.Movie },
+ DtoOptions = new DtoOptions(false)
{
- _libraryMonitor.ReportFileSystemChanged(item.Path);
+ EnableImages = false
}
+ });
- return NoContent();
+ if (!string.IsNullOrWhiteSpace(imdbId))
+ {
+ movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ else if (!string.IsNullOrWhiteSpace(tmdbId))
+ {
+ movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ else
+ {
+ movies = new List<BaseItem>();
}
- /// <summary>
- /// Reports that new movies have been added by an external source.
- /// </summary>
- /// <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 dto)
+ foreach (var item in movies)
{
- foreach (var item in dto.Updates)
- {
- _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
- }
+ _libraryMonitor.ReportFileSystemChanged(item.Path);
+ }
- return NoContent();
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Reports that new movies have been added by an external source.
+ /// </summary>
+ /// <param name="dto">The update paths.</param>
+ /// <response code="204">Report success.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Media/Updated")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
+ {
+ foreach (var item in dto.Updates)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
}
- /// <summary>
- /// Downloads item media.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="200">Media downloaded.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="FileResult"/> containing the media stream.</returns>
- /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
- [HttpGet("Items/{itemId}/Download")]
- [Authorize(Policy = Policies.Download)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesFile("video/*", "audio/*")]
- public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ return NoContent();
+ }
- var user = _userManager.GetUserById(User.GetUserId());
+ /// <summary>
+ /// Downloads item media.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="200">Media downloaded.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="FileResult"/> containing the media stream.</returns>
+ /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
+ [HttpGet("Items/{itemId}/Download")]
+ [Authorize(Policy = Policies.Download)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile("video/*", "audio/*")]
+ public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- if (user is not null)
+ var user = _userManager.GetUserById(User.GetUserId());
+
+ if (user is not null)
+ {
+ if (!item.CanDownload(user))
{
- if (!item.CanDownload(user))
- {
- throw new ArgumentException("Item does not support downloading");
- }
+ throw new ArgumentException("Item does not support downloading");
}
- else
+ }
+ else
+ {
+ if (!item.CanDownload())
{
- if (!item.CanDownload())
- {
- throw new ArgumentException("Item does not support downloading");
- }
+ throw new ArgumentException("Item does not support downloading");
}
+ }
- if (user is not null)
- {
- await LogDownloadAsync(item, user).ConfigureAwait(false);
- }
+ if (user is not null)
+ {
+ await LogDownloadAsync(item, user).ConfigureAwait(false);
+ }
- // Quotes are valid in linux. They'll possibly cause issues here.
- var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
-
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true);
- }
-
- /// <summary>
- /// Gets similar items.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="excludeArtistIds">Exclude artist ids.</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. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
- /// <response code="200">Similar items returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
- [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
- [HttpGet("Items/{itemId}/Similar")]
- [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
- [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
- [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
- [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
- [FromRoute, Required] Guid itemId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery] Guid? userId,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
- {
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
- if (item is Episode || (item is IItemByName && item is not MusicArtist))
- {
- return new QueryResult<BaseItemDto>();
- }
+ // Quotes are valid in linux. They'll possibly cause issues here.
+ var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User);
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true);
+ }
- var program = item as IHasProgramAttributes;
- bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
- bool? isSeries = item is Series || (program is not null && program.IsSeries);
+ /// <summary>
+ /// Gets similar items.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="excludeArtistIds">Exclude artist ids.</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. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <response code="200">Similar items returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
+ [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
+ [HttpGet("Items/{itemId}/Similar")]
+ [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
+ [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
+ [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
+ [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var item = itemId.Equals(default)
+ ? (userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
- var includeItemTypes = new List<BaseItemKind>();
- if (isMovie.Value)
- {
- includeItemTypes.Add(BaseItemKind.Movie);
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- includeItemTypes.Add(BaseItemKind.Trailer);
- includeItemTypes.Add(BaseItemKind.LiveTvProgram);
- }
- }
- else if (isSeries.Value)
- {
- includeItemTypes.Add(BaseItemKind.Series);
- }
- else
- {
- // For non series and movie types these columns are typically null
- // isSeries = null;
- isMovie = null;
- includeItemTypes.Add(item.GetBaseItemKind());
- }
+ if (item is Episode || (item is IItemByName && item is not MusicArtist))
+ {
+ return new QueryResult<BaseItemDto>();
+ }
- var query = new InternalItemsQuery(user)
- {
- Genres = item.Genres,
- Limit = limit,
- IncludeItemTypes = includeItemTypes.ToArray(),
- SimilarTo = item,
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = !isMovie ?? true,
- EnableGroupByMetadataKey = isMovie ?? false,
- MinSimilarityScore = 2 // A remnant from album/artist scoring
- };
-
- // ExcludeArtistIds
- if (excludeArtistIds.Length != 0)
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User);
+
+ var program = item as IHasProgramAttributes;
+ bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
+ bool? isSeries = item is Series || (program is not null && program.IsSeries);
+
+ var includeItemTypes = new List<BaseItemKind>();
+ if (isMovie.Value)
+ {
+ includeItemTypes.Add(BaseItemKind.Movie);
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
- query.ExcludeArtistIds = excludeArtistIds;
+ includeItemTypes.Add(BaseItemKind.Trailer);
+ includeItemTypes.Add(BaseItemKind.LiveTvProgram);
}
+ }
+ else if (isSeries.Value)
+ {
+ includeItemTypes.Add(BaseItemKind.Series);
+ }
+ else
+ {
+ // For non series and movie types these columns are typically null
+ // isSeries = null;
+ isMovie = null;
+ includeItemTypes.Add(item.GetBaseItemKind());
+ }
- List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
+ var query = new InternalItemsQuery(user)
+ {
+ Genres = item.Genres,
+ Limit = limit,
+ IncludeItemTypes = includeItemTypes.ToArray(),
+ SimilarTo = item,
+ DtoOptions = dtoOptions,
+ EnableTotalRecordCount = !isMovie ?? true,
+ EnableGroupByMetadataKey = isMovie ?? false,
+ MinSimilarityScore = 2 // A remnant from album/artist scoring
+ };
+
+ // ExcludeArtistIds
+ if (excludeArtistIds.Length != 0)
+ {
+ query.ExcludeArtistIds = excludeArtistIds;
+ }
- var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
+ List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
- return new QueryResult<BaseItemDto>(
- query.StartIndex,
- itemsResult.Count,
- returnList);
- }
+ var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
- /// <summary>
- /// Gets the library options info.
- /// </summary>
- /// <param name="libraryContentType">Library content type.</param>
- /// <param name="isNewLibrary">Whether this is a new library.</param>
- /// <response code="200">Library options info returned.</response>
- /// <returns>Library options info.</returns>
- [HttpGet("Libraries/AvailableOptions")]
- [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
- [FromQuery] string? libraryContentType,
- [FromQuery] bool isNewLibrary = false)
- {
- var result = new LibraryOptionsResultDto();
+ return new QueryResult<BaseItemDto>(
+ query.StartIndex,
+ itemsResult.Count,
+ returnList);
+ }
- var types = GetRepresentativeItemTypes(libraryContentType);
- var typesList = types.ToList();
+ /// <summary>
+ /// Gets the library options info.
+ /// </summary>
+ /// <param name="libraryContentType">Library content type.</param>
+ /// <param name="isNewLibrary">Whether this is a new library.</param>
+ /// <response code="200">Library options info returned.</response>
+ /// <returns>Library options info.</returns>
+ [HttpGet("Libraries/AvailableOptions")]
+ [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
+ [FromQuery] string? libraryContentType,
+ [FromQuery] bool isNewLibrary = false)
+ {
+ var result = new LibraryOptionsResultDto();
- var plugins = _providerManager.GetAllMetadataPlugins()
- .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
- .OrderBy(i => typesList.IndexOf(i.ItemType))
- .ToList();
+ var types = GetRepresentativeItemTypes(libraryContentType);
+ var typesList = types.ToList();
- result.MetadataSavers = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- result.MetadataReaders = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = true
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- result.SubtitleFetchers = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = true
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray();
+ var plugins = _providerManager.GetAllMetadataPlugins()
+ .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(i => typesList.IndexOf(i.ItemType))
+ .ToList();
- var typeOptions = new List<LibraryTypeOptionsDto>();
+ result.MetadataSavers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
- foreach (var type in types)
+ result.MetadataReaders = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
+ .Select(i => new LibraryOptionInfoDto
{
- TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
- typeOptions.Add(new LibraryTypeOptionsDto
- {
- Type = type,
+ result.SubtitleFetchers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ var typeOptions = new List<LibraryTypeOptionsDto>();
+
+ foreach (var type in types)
+ {
+ TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
+
+ typeOptions.Add(new LibraryTypeOptionsDto
+ {
+ Type = type,
- MetadataFetchers = plugins
+ MetadataFetchers = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
.Select(i => new LibraryOptionInfoDto
@@ -814,7 +832,7 @@ namespace Jellyfin.Api.Controllers
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
- ImageFetchers = plugins
+ ImageFetchers = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
.Select(i => new LibraryOptionInfoDto
@@ -825,148 +843,147 @@ namespace Jellyfin.Api.Controllers
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
- SupportedImageTypes = plugins
+ SupportedImageTypes = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
.Distinct()
.ToArray(),
- DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
- });
- }
+ DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
+ });
+ }
- result.TypeOptions = typeOptions.ToArray();
+ result.TypeOptions = typeOptions.ToArray();
- return result;
- }
+ return result;
+ }
- private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
+ private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
+ {
+ var query = new InternalItemsQuery(user)
{
- var query = new InternalItemsQuery(user)
+ IncludeItemTypes = new[] { itemKind },
+ Limit = 0,
+ Recursive = true,
+ IsVirtualItem = false,
+ IsFavorite = isFavorite,
+ DtoOptions = new DtoOptions(false)
{
- IncludeItemTypes = new[] { itemKind },
- Limit = 0,
- Recursive = true,
- IsVirtualItem = false,
- IsFavorite = isFavorite,
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- };
+ EnableImages = false
+ }
+ };
- return _libraryManager.GetItemsResult(query).TotalRecordCount;
- }
+ return _libraryManager.GetItemsResult(query).TotalRecordCount;
+ }
- private BaseItem? TranslateParentItem(BaseItem item, User user)
- {
- return item.GetParent() is AggregateFolder
- ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
- .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
- : item;
- }
+ private BaseItem? TranslateParentItem(BaseItem item, User user)
+ {
+ return item.GetParent() is AggregateFolder
+ ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
+ : item;
+ }
- private async Task LogDownloadAsync(BaseItem item, User user)
+ private async Task LogDownloadAsync(BaseItem item, User user)
+ {
+ try
{
- try
- {
- await _activityManager.CreateAsync(new ActivityLog(
- string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
- "UserDownloadingContent",
- User.GetUserId())
- {
- ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
- }).ConfigureAwait(false);
- }
- catch
+ await _activityManager.CreateAsync(new ActivityLog(
+ string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
+ "UserDownloadingContent",
+ User.GetUserId())
{
- // Logged at lower levels
- }
+ ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
+ }).ConfigureAwait(false);
}
-
- private static string[] GetRepresentativeItemTypes(string? contentType)
+ catch
{
- return contentType switch
- {
- CollectionType.BoxSets => new[] { "BoxSet" },
- CollectionType.Playlists => new[] { "Playlist" },
- CollectionType.Movies => new[] { "Movie" },
- CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
- CollectionType.Books => new[] { "Book" },
- CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
- CollectionType.HomeVideos => new[] { "Video", "Photo" },
- CollectionType.Photos => new[] { "Video", "Photo" },
- CollectionType.MusicVideos => new[] { "MusicVideo" },
- _ => new[] { "Series", "Season", "Episode", "Movie" }
- };
- }
-
- private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
- {
- if (isNewLibrary)
- {
- return false;
- }
+ // Logged at lower levels
+ }
+ }
- var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
- .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- .ToArray();
+ private static string[] GetRepresentativeItemTypes(string? contentType)
+ {
+ return contentType switch
+ {
+ CollectionType.BoxSets => new[] { "BoxSet" },
+ CollectionType.Playlists => new[] { "Playlist" },
+ CollectionType.Movies => new[] { "Movie" },
+ CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
+ CollectionType.Books => new[] { "Book" },
+ CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
+ CollectionType.HomeVideos => new[] { "Video", "Photo" },
+ CollectionType.Photos => new[] { "Video", "Photo" },
+ CollectionType.MusicVideos => new[] { "MusicVideo" },
+ _ => new[] { "Series", "Season", "Episode", "Movie" }
+ };
+ }
- return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
+ private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
+ {
+ if (isNewLibrary)
+ {
+ return false;
}
- private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ {
+ if (isNewLibrary)
{
- if (isNewLibrary)
+ if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
- {
- return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+ return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
|| string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
|| string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
- }
+ }
- return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+ return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
- }
+ }
- var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .ToArray();
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
- return metadataOptions.Length == 0
+ return metadataOptions.Length == 0
|| metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
- }
+ }
- private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ {
+ if (isNewLibrary)
{
- if (isNewLibrary)
+ if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
- {
- return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
- }
-
- return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
+ return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
}
- var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .ToArray();
+ return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
+ }
- if (metadataOptions.Length == 0)
- {
- return true;
- }
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
- return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
+ if (metadataOptions.Length == 0)
+ {
+ return true;
}
+
+ return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
}
}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 1c2394055..b012ff42e 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -20,308 +20,307 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The library structure controller.
+/// </summary>
+[Route("Library/VirtualFolders")]
+[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+public class LibraryStructureController : BaseJellyfinApiController
{
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryMonitor _libraryMonitor;
+
/// <summary>
- /// The library structure controller.
+ /// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
/// </summary>
- [Route("Library/VirtualFolders")]
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- public class LibraryStructureController : BaseJellyfinApiController
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
+ public LibraryStructureController(
+ IServerConfigurationManager serverConfigurationManager,
+ ILibraryManager libraryManager,
+ ILibraryMonitor libraryMonitor)
{
- private readonly IServerApplicationPaths _appPaths;
- private readonly ILibraryManager _libraryManager;
- private readonly ILibraryMonitor _libraryMonitor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
- public LibraryStructureController(
- IServerConfigurationManager serverConfigurationManager,
- ILibraryManager libraryManager,
- ILibraryMonitor libraryMonitor)
- {
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _libraryManager = libraryManager;
- _libraryMonitor = libraryMonitor;
- }
+ _appPaths = serverConfigurationManager.ApplicationPaths;
+ _libraryManager = libraryManager;
+ _libraryMonitor = libraryMonitor;
+ }
+
+ /// <summary>
+ /// Gets all virtual folders.
+ /// </summary>
+ /// <response code="200">Virtual folders retrieved.</response>
+ /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
+ {
+ return _libraryManager.GetVirtualFolders(true);
+ }
+
+ /// <summary>
+ /// Adds a virtual folder.
+ /// </summary>
+ /// <param name="name">The name of the virtual folder.</param>
+ /// <param name="collectionType">The type of the collection.</param>
+ /// <param name="paths">The paths of the virtual folder.</param>
+ /// <param name="libraryOptionsDto">The library options.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <response code="204">Folder added.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> AddVirtualFolder(
+ [FromQuery] string? name,
+ [FromQuery] CollectionTypeOptions? collectionType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
+ [FromBody] AddVirtualFolderDto? libraryOptionsDto,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
- /// <summary>
- /// Gets all virtual folders.
- /// </summary>
- /// <response code="200">Virtual folders retrieved.</response>
- /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
+ if (paths is not null && paths.Length > 0)
{
- return _libraryManager.GetVirtualFolders(true);
+ libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
}
- /// <summary>
- /// Adds a virtual folder.
- /// </summary>
- /// <param name="name">The name of the virtual folder.</param>
- /// <param name="collectionType">The type of the collection.</param>
- /// <param name="paths">The paths of the virtual folder.</param>
- /// <param name="libraryOptionsDto">The library options.</param>
- /// <param name="refreshLibrary">Whether to refresh the library.</param>
- /// <response code="204">Folder added.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddVirtualFolder(
- [FromQuery] string? name,
- [FromQuery] CollectionTypeOptions? collectionType,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
- [FromBody] AddVirtualFolderDto? libraryOptionsDto,
- [FromQuery] bool refreshLibrary = false)
- {
- var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
+ await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
- if (paths is not null && paths.Length > 0)
- {
- libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
- }
+ return NoContent();
+ }
- await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
+ /// <summary>
+ /// Removes a virtual folder.
+ /// </summary>
+ /// <param name="name">The name of the folder.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <response code="204">Folder removed.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RemoveVirtualFolder(
+ [FromQuery] string? name,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
+ /// <summary>
+ /// Renames a virtual folder.
+ /// </summary>
+ /// <param name="name">The name of the virtual folder.</param>
+ /// <param name="newName">The new name.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <response code="204">Folder renamed.</response>
+ /// <response code="404">Library doesn't exist.</response>
+ /// <response code="409">Library already exists.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
+ /// <exception cref="ArgumentNullException">The new name may not be null.</exception>
+ [HttpPost("Name")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public ActionResult RenameVirtualFolder(
+ [FromQuery] string? name,
+ [FromQuery] string? newName,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException(nameof(name));
}
- /// <summary>
- /// Removes a virtual folder.
- /// </summary>
- /// <param name="name">The name of the folder.</param>
- /// <param name="refreshLibrary">Whether to refresh the library.</param>
- /// <response code="204">Folder removed.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveVirtualFolder(
- [FromQuery] string? name,
- [FromQuery] bool refreshLibrary = false)
+ if (string.IsNullOrWhiteSpace(newName))
{
- await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
- return NoContent();
+ throw new ArgumentNullException(nameof(newName));
}
- /// <summary>
- /// Renames a virtual folder.
- /// </summary>
- /// <param name="name">The name of the virtual folder.</param>
- /// <param name="newName">The new name.</param>
- /// <param name="refreshLibrary">Whether to refresh the library.</param>
- /// <response code="204">Folder renamed.</response>
- /// <response code="404">Library doesn't exist.</response>
- /// <response code="409">Library already exists.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
- /// <exception cref="ArgumentNullException">The new name may not be null.</exception>
- [HttpPost("Name")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status409Conflict)]
- public ActionResult RenameVirtualFolder(
- [FromQuery] string? name,
- [FromQuery] string? newName,
- [FromQuery] bool refreshLibrary = false)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
+ var rootFolderPath = _appPaths.DefaultUserViewsPath;
- if (string.IsNullOrWhiteSpace(newName))
- {
- throw new ArgumentNullException(nameof(newName));
- }
+ var currentPath = Path.Combine(rootFolderPath, name);
+ var newPath = Path.Combine(rootFolderPath, newName);
- var rootFolderPath = _appPaths.DefaultUserViewsPath;
+ if (!Directory.Exists(currentPath))
+ {
+ return NotFound("The media collection does not exist.");
+ }
- var currentPath = Path.Combine(rootFolderPath, name);
- var newPath = Path.Combine(rootFolderPath, newName);
+ if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
+ {
+ return Conflict($"The media library already exists at {newPath}.");
+ }
- if (!Directory.Exists(currentPath))
- {
- return NotFound("The media collection does not exist.");
- }
+ _libraryMonitor.Stop();
- if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
+ try
+ {
+ // Changing capitalization. Handle windows case insensitivity
+ if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
{
- return Conflict($"The media library already exists at {newPath}.");
+ var tempPath = Path.Combine(
+ rootFolderPath,
+ Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
+ Directory.Move(currentPath, tempPath);
+ currentPath = tempPath;
}
- _libraryMonitor.Stop();
+ Directory.Move(currentPath, newPath);
+ }
+ finally
+ {
+ CollectionFolder.OnCollectionFolderChange();
- try
+ Task.Run(async () =>
{
- // Changing capitalization. Handle windows case insensitivity
- if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
{
- var tempPath = Path.Combine(
- rootFolderPath,
- Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
- Directory.Move(currentPath, tempPath);
- currentPath = tempPath;
+ await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
}
-
- Directory.Move(currentPath, newPath);
- }
- finally
- {
- CollectionFolder.OnCollectionFolderChange();
-
- Task.Run(async () =>
+ else
{
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
- {
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- // Have to block here to allow exceptions to bubble
- await Task.Delay(1000).ConfigureAwait(false);
- _libraryMonitor.Start();
- }
- });
- }
-
- return NoContent();
+ // Need to add a delay here or directory watchers may still pick up the changes
+ // Have to block here to allow exceptions to bubble
+ await Task.Delay(1000).ConfigureAwait(false);
+ _libraryMonitor.Start();
+ }
+ });
}
- /// <summary>
- /// Add a media path to a library.
- /// </summary>
- /// <param name="mediaPathDto">The media path dto.</param>
- /// <param name="refreshLibrary">Whether to refresh the library.</param>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- /// <response code="204">Media path added.</response>
- /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
- [HttpPost("Paths")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult AddMediaPath(
- [FromBody, Required] MediaPathDto mediaPathDto,
- [FromQuery] bool refreshLibrary = false)
- {
- _libraryMonitor.Stop();
+ return NoContent();
+ }
- try
- {
- var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null."));
+ /// <summary>
+ /// Add a media path to a library.
+ /// </summary>
+ /// <param name="mediaPathDto">The media path dto.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ /// <response code="204">Media path added.</response>
+ /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+ [HttpPost("Paths")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult AddMediaPath(
+ [FromBody, Required] MediaPathDto mediaPathDto,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ _libraryMonitor.Stop();
- _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
- }
- finally
- {
- Task.Run(async () =>
- {
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
- {
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- // Have to block here to allow exceptions to bubble
- await Task.Delay(1000).ConfigureAwait(false);
- _libraryMonitor.Start();
- }
- });
- }
+ try
+ {
+ var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null."));
- return NoContent();
+ _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
}
-
- /// <summary>
- /// Updates a media path.
- /// </summary>
- /// <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([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
+ finally
{
- if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
+ Task.Run(async () =>
{
- throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
- }
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
+ {
+ await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ // Have to block here to allow exceptions to bubble
+ await Task.Delay(1000).ConfigureAwait(false);
+ _libraryMonitor.Start();
+ }
+ });
+ }
+
+ return NoContent();
+ }
- _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
- return NoContent();
+ /// <summary>
+ /// Updates a media path.
+ /// </summary>
+ /// <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([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
+ {
+ if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
+ {
+ throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
}
- /// <summary>
- /// Remove a media path.
- /// </summary>
- /// <param name="name">The name of the library.</param>
- /// <param name="path">The path to remove.</param>
- /// <param name="refreshLibrary">Whether to refresh the library.</param>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- /// <response code="204">Media path removed.</response>
- /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
- [HttpDelete("Paths")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult RemoveMediaPath(
- [FromQuery] string? name,
- [FromQuery] string? path,
- [FromQuery] bool refreshLibrary = false)
+ _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Remove a media path.
+ /// </summary>
+ /// <param name="name">The name of the library.</param>
+ /// <param name="path">The path to remove.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ /// <response code="204">Media path removed.</response>
+ /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+ [HttpDelete("Paths")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult RemoveMediaPath(
+ [FromQuery] string? name,
+ [FromQuery] string? path,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ if (string.IsNullOrWhiteSpace(name))
{
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
+ throw new ArgumentNullException(nameof(name));
+ }
- _libraryMonitor.Stop();
+ _libraryMonitor.Stop();
- try
- {
- _libraryManager.RemoveMediaPath(name, path);
- }
- finally
+ try
+ {
+ _libraryManager.RemoveMediaPath(name, path);
+ }
+ finally
+ {
+ Task.Run(async () =>
{
- Task.Run(async () =>
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
{
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
- {
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- // Have to block here to allow exceptions to bubble
- await Task.Delay(1000).ConfigureAwait(false);
- _libraryMonitor.Start();
- }
- });
- }
-
- return NoContent();
+ await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ // Have to block here to allow exceptions to bubble
+ await Task.Delay(1000).ConfigureAwait(false);
+ _libraryMonitor.Start();
+ }
+ });
}
- /// <summary>
- /// Update library options.
- /// </summary>
- /// <param name="request">The library name and options.</param>
- /// <response code="204">Library updated.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("LibraryOptions")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateLibraryOptions(
- [FromBody] UpdateLibraryOptionsDto request)
- {
- var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
+ return NoContent();
+ }
- collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
- return NoContent();
- }
+ /// <summary>
+ /// Update library options.
+ /// </summary>
+ /// <param name="request">The library name and options.</param>
+ /// <response code="204">Library updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("LibraryOptions")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateLibraryOptions(
+ [FromBody] UpdateLibraryOptionsDto request)
+ {
+ var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
+
+ collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 5228e0bab..267ba4afb 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -17,14 +17,12 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -35,1200 +33,1176 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Live tv controller.
+/// </summary>
+public class LiveTvController : BaseJellyfinApiController
{
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly IUserManager _userManager;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IConfigurationManager _configurationManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ISessionManager _sessionManager;
+
/// <summary>
- /// Live tv controller.
+ /// Initializes a new instance of the <see cref="LiveTvController"/> class.
/// </summary>
- public class LiveTvController : BaseJellyfinApiController
+ /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ public LiveTvController(
+ ILiveTvManager liveTvManager,
+ IUserManager userManager,
+ IHttpClientFactory httpClientFactory,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IMediaSourceManager mediaSourceManager,
+ IConfigurationManager configurationManager,
+ TranscodingJobHelper transcodingJobHelper,
+ ISessionManager sessionManager)
{
- private readonly ILiveTvManager _liveTvManager;
- private readonly IUserManager _userManager;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IConfigurationManager _configurationManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LiveTvController"/> class.
- /// </summary>
- /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- public LiveTvController(
- ILiveTvManager liveTvManager,
- IUserManager userManager,
- IHttpClientFactory httpClientFactory,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IMediaSourceManager mediaSourceManager,
- IConfigurationManager configurationManager,
- TranscodingJobHelper transcodingJobHelper,
- ISessionManager sessionManager)
- {
- _liveTvManager = liveTvManager;
- _userManager = userManager;
- _httpClientFactory = httpClientFactory;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _mediaSourceManager = mediaSourceManager;
- _configurationManager = configurationManager;
- _transcodingJobHelper = transcodingJobHelper;
- _sessionManager = sessionManager;
- }
+ _liveTvManager = liveTvManager;
+ _userManager = userManager;
+ _httpClientFactory = httpClientFactory;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _mediaSourceManager = mediaSourceManager;
+ _configurationManager = configurationManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _sessionManager = sessionManager;
+ }
- /// <summary>
- /// Gets available live tv services.
- /// </summary>
- /// <response code="200">Available live tv services returned.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the available live tv services.
- /// </returns>
- [HttpGet("Info")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<LiveTvInfo> GetLiveTvInfo()
- {
- return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
- }
+ /// <summary>
+ /// Gets available live tv services.
+ /// </summary>
+ /// <response code="200">Available live tv services returned.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the available live tv services.
+ /// </returns>
+ [HttpGet("Info")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<LiveTvInfo> GetLiveTvInfo()
+ {
+ return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
+ }
- /// <summary>
- /// Gets available live tv channels.
- /// </summary>
- /// <param name="type">Optional. Filter by channel type.</param>
- /// <param name="userId">Optional. Filter by user and attach user data.</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="isMovie">Optional. Filter for movies.</param>
- /// <param name="isSeries">Optional. Filter for series.</param>
- /// <param name="isNews">Optional. Filter for news.</param>
- /// <param name="isKids">Optional. Filter for kids.</param>
- /// <param name="isSports">Optional. Filter for sports.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="isFavorite">Optional. Filter by channels that are favorites, or not.</param>
- /// <param name="isLiked">Optional. Filter by channels that are liked, or not.</param>
- /// <param name="isDisliked">Optional. Filter by channels that are disliked, or not.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="sortBy">Optional. Key to sort by.</param>
- /// <param name="sortOrder">Optional. Sort order.</param>
- /// <param name="enableFavoriteSorting">Optional. Incorporate favorite and like status into channel sorting.</param>
- /// <param name="addCurrentProgram">Optional. Adds current program info to each channel.</param>
- /// <response code="200">Available live tv channels returned.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the resulting available live tv channels.
- /// </returns>
- [HttpGet("Channels")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
- [FromQuery] ChannelType? type,
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] int? limit,
- [FromQuery] bool? isFavorite,
- [FromQuery] bool? isLiked,
- [FromQuery] bool? isDisliked,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] SortOrder? sortOrder,
- [FromQuery] bool enableFavoriteSorting = false,
- [FromQuery] bool addCurrentProgram = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- var channelResult = _liveTvManager.GetInternalChannels(
- new LiveTvChannelQuery
- {
- ChannelType = type,
- UserId = userId ?? Guid.Empty,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- IsLiked = isLiked,
- IsDisliked = isDisliked,
- EnableFavoriteSorting = enableFavoriteSorting,
- IsMovie = isMovie,
- IsSeries = isSeries,
- IsNews = isNews,
- IsKids = isKids,
- IsSports = isSports,
- SortBy = sortBy,
- SortOrder = sortOrder ?? SortOrder.Ascending,
- AddCurrentProgram = addCurrentProgram
- },
- dtoOptions,
- CancellationToken.None);
-
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var fieldsList = dtoOptions.Fields.ToList();
- fieldsList.Remove(ItemFields.CanDelete);
- fieldsList.Remove(ItemFields.CanDownload);
- fieldsList.Remove(ItemFields.DisplayPreferencesId);
- fieldsList.Remove(ItemFields.Etag);
- dtoOptions.Fields = fieldsList.ToArray();
- dtoOptions.AddCurrentProgram = addCurrentProgram;
-
- var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user);
- return new QueryResult<BaseItemDto>(
- startIndex,
- channelResult.TotalRecordCount,
- returnArray);
- }
+ /// <summary>
+ /// Gets available live tv channels.
+ /// </summary>
+ /// <param name="type">Optional. Filter by channel type.</param>
+ /// <param name="userId">Optional. Filter by user and attach user data.</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="isMovie">Optional. Filter for movies.</param>
+ /// <param name="isSeries">Optional. Filter for series.</param>
+ /// <param name="isNews">Optional. Filter for news.</param>
+ /// <param name="isKids">Optional. Filter for kids.</param>
+ /// <param name="isSports">Optional. Filter for sports.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="isFavorite">Optional. Filter by channels that are favorites, or not.</param>
+ /// <param name="isLiked">Optional. Filter by channels that are liked, or not.</param>
+ /// <param name="isDisliked">Optional. Filter by channels that are disliked, or not.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="sortBy">Optional. Key to sort by.</param>
+ /// <param name="sortOrder">Optional. Sort order.</param>
+ /// <param name="enableFavoriteSorting">Optional. Incorporate favorite and like status into channel sorting.</param>
+ /// <param name="addCurrentProgram">Optional. Adds current program info to each channel.</param>
+ /// <response code="200">Available live tv channels returned.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the resulting available live tv channels.
+ /// </returns>
+ [HttpGet("Channels")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
+ [FromQuery] ChannelType? type,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] int? limit,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] bool? isLiked,
+ [FromQuery] bool? isDisliked,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] SortOrder? sortOrder,
+ [FromQuery] bool enableFavoriteSorting = false,
+ [FromQuery] bool addCurrentProgram = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- /// <summary>
- /// Gets a live tv channel.
- /// </summary>
- /// <param name="channelId">Channel id.</param>
- /// <param name="userId">Optional. Attach user data.</param>
- /// <response code="200">Live tv channel returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
- [HttpGet("Channels/{channelId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = channelId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(channelId);
-
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ var channelResult = _liveTvManager.GetInternalChannels(
+ new LiveTvChannelQuery
+ {
+ ChannelType = type,
+ UserId = userId.Value,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ IsLiked = isLiked,
+ IsDisliked = isDisliked,
+ EnableFavoriteSorting = enableFavoriteSorting,
+ IsMovie = isMovie,
+ IsSeries = isSeries,
+ IsNews = isNews,
+ IsKids = isKids,
+ IsSports = isSports,
+ SortBy = sortBy,
+ SortOrder = sortOrder ?? SortOrder.Ascending,
+ AddCurrentProgram = addCurrentProgram
+ },
+ dtoOptions,
+ CancellationToken.None);
+
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var fieldsList = dtoOptions.Fields.ToList();
+ fieldsList.Remove(ItemFields.CanDelete);
+ fieldsList.Remove(ItemFields.CanDownload);
+ fieldsList.Remove(ItemFields.DisplayPreferencesId);
+ fieldsList.Remove(ItemFields.Etag);
+ dtoOptions.Fields = fieldsList.ToArray();
+ dtoOptions.AddCurrentProgram = addCurrentProgram;
+
+ var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user);
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ channelResult.TotalRecordCount,
+ returnArray);
+ }
- /// <summary>
- /// Gets live tv recordings.
- /// </summary>
- /// <param name="channelId">Optional. Filter by channel id.</param>
- /// <param name="userId">Optional. Filter by user and attach user data.</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="status">Optional. Filter by recording status.</param>
- /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
- /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="isMovie">Optional. Filter for movies.</param>
- /// <param name="isSeries">Optional. Filter for series.</param>
- /// <param name="isKids">Optional. Filter for kids.</param>
- /// <param name="isSports">Optional. Filter for sports.</param>
- /// <param name="isNews">Optional. Filter for news.</param>
- /// <param name="isLibraryItem">Optional. Filter for is library item.</param>
- /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
- /// <response code="200">Live tv recordings returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
- [HttpGet("Recordings")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
- [FromQuery] string? channelId,
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] RecordingStatus? status,
- [FromQuery] bool? isInProgress,
- [FromQuery] string? seriesTimerId,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isLibraryItem,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- return _liveTvManager.GetRecordings(
- new RecordingQuery
- {
- ChannelId = channelId,
- UserId = userId ?? Guid.Empty,
- StartIndex = startIndex,
- Limit = limit,
- Status = status,
- SeriesTimerId = seriesTimerId,
- IsInProgress = isInProgress,
- EnableTotalRecordCount = enableTotalRecordCount,
- IsMovie = isMovie,
- IsNews = isNews,
- IsSeries = isSeries,
- IsKids = isKids,
- IsSports = isSports,
- IsLibraryItem = isLibraryItem,
- Fields = fields,
- ImageTypeLimit = imageTypeLimit,
- EnableImages = enableImages
- },
- dtoOptions);
- }
+ /// <summary>
+ /// Gets a live tv channel.
+ /// </summary>
+ /// <param name="channelId">Channel id.</param>
+ /// <param name="userId">Optional. Attach user data.</param>
+ /// <response code="200">Live tv channel returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
+ [HttpGet("Channels/{channelId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = channelId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(channelId);
+
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- /// <summary>
- /// Gets live tv recording series.
- /// </summary>
- /// <param name="channelId">Optional. Filter by channel id.</param>
- /// <param name="userId">Optional. Filter by user and attach user data.</param>
- /// <param name="groupId">Optional. Filter by recording group.</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="status">Optional. Filter by recording status.</param>
- /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
- /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
- /// <response code="200">Live tv recordings returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
- [HttpGet("Recordings/Series")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
- [FromQuery] string? channelId,
- [FromQuery] Guid? userId,
- [FromQuery] string? groupId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] RecordingStatus? status,
- [FromQuery] bool? isInProgress,
- [FromQuery] string? seriesTimerId,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- return new QueryResult<BaseItemDto>();
- }
+ /// <summary>
+ /// Gets live tv recordings.
+ /// </summary>
+ /// <param name="channelId">Optional. Filter by channel id.</param>
+ /// <param name="userId">Optional. Filter by user and attach user data.</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="status">Optional. Filter by recording status.</param>
+ /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
+ /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="isMovie">Optional. Filter for movies.</param>
+ /// <param name="isSeries">Optional. Filter for series.</param>
+ /// <param name="isKids">Optional. Filter for kids.</param>
+ /// <param name="isSports">Optional. Filter for sports.</param>
+ /// <param name="isNews">Optional. Filter for news.</param>
+ /// <param name="isLibraryItem">Optional. Filter for is library item.</param>
+ /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
+ /// <response code="200">Live tv recordings returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
+ [HttpGet("Recordings")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordings(
+ [FromQuery] string? channelId,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] RecordingStatus? status,
+ [FromQuery] bool? isInProgress,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isLibraryItem,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- /// <summary>
- /// Gets live tv recording groups.
- /// </summary>
- /// <param name="userId">Optional. Filter by user and attach user data.</param>
- /// <response code="200">Recording groups returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
- [HttpGet("Recordings/Groups")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
- {
- return new QueryResult<BaseItemDto>();
- }
+ return await _liveTvManager.GetRecordingsAsync(
+ new RecordingQuery
+ {
+ ChannelId = channelId,
+ UserId = userId.Value,
+ StartIndex = startIndex,
+ Limit = limit,
+ Status = status,
+ SeriesTimerId = seriesTimerId,
+ IsInProgress = isInProgress,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ IsMovie = isMovie,
+ IsNews = isNews,
+ IsSeries = isSeries,
+ IsKids = isKids,
+ IsSports = isSports,
+ IsLibraryItem = isLibraryItem,
+ Fields = fields,
+ ImageTypeLimit = imageTypeLimit,
+ EnableImages = enableImages
+ },
+ dtoOptions).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets recording folders.
- /// </summary>
- /// <param name="userId">Optional. Filter by user and attach user data.</param>
- /// <response code="200">Recording folders returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
- [HttpGet("Recordings/Folders")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var folders = _liveTvManager.GetRecordingFolders(user);
+ /// <summary>
+ /// Gets live tv recording series.
+ /// </summary>
+ /// <param name="channelId">Optional. Filter by channel id.</param>
+ /// <param name="userId">Optional. Filter by user and attach user data.</param>
+ /// <param name="groupId">Optional. Filter by recording group.</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="status">Optional. Filter by recording status.</param>
+ /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
+ /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
+ /// <response code="200">Live tv recordings returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
+ [HttpGet("Recordings/Series")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [Obsolete("This endpoint is obsolete.")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
+ public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
+ [FromQuery] string? channelId,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? groupId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] RecordingStatus? status,
+ [FromQuery] bool? isInProgress,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ return new QueryResult<BaseItemDto>();
+ }
- var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
+ /// <summary>
+ /// Gets live tv recording groups.
+ /// </summary>
+ /// <param name="userId">Optional. Filter by user and attach user data.</param>
+ /// <response code="200">Recording groups returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
+ [HttpGet("Recordings/Groups")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [Obsolete("This endpoint is obsolete.")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
+ {
+ return new QueryResult<BaseItemDto>();
+ }
- return new QueryResult<BaseItemDto>(returnArray);
- }
+ /// <summary>
+ /// Gets recording folders.
+ /// </summary>
+ /// <param name="userId">Optional. Filter by user and attach user data.</param>
+ /// <response code="200">Recording folders returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
+ [HttpGet("Recordings/Folders")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false);
- /// <summary>
- /// Gets a live tv recording.
- /// </summary>
- /// <param name="recordingId">Recording id.</param>
- /// <param name="userId">Optional. Attach user data.</param>
- /// <response code="200">Recording returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
- [HttpGet("Recordings/{recordingId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+ var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
+ return new QueryResult<BaseItemDto>(returnArray);
+ }
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ /// <summary>
+ /// Gets a live tv recording.
+ /// </summary>
+ /// <param name="recordingId">Recording id.</param>
+ /// <param name="userId">Optional. Attach user data.</param>
+ /// <response code="200">Recording returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
+ [HttpGet("Recordings/{recordingId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
- /// <summary>
- /// Resets a tv tuner.
- /// </summary>
- /// <param name="tunerId">Tuner id.</param>
- /// <response code="204">Tuner reset.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Tuners/{tunerId}/Reset")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
- /// <summary>
- /// Gets a timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="200">Timer returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the timer.
- /// </returns>
- [HttpGet("Timers/{timerId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
- {
- return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
- }
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- /// <summary>
- /// Gets the default values for a new timer.
- /// </summary>
- /// <param name="programId">Optional. To attach default values based on a program.</param>
- /// <response code="200">Default values returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the default values for a timer.
- /// </returns>
- [HttpGet("Timers/Defaults")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
- {
- return string.IsNullOrEmpty(programId)
- ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false)
- : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Resets a tv tuner.
+ /// </summary>
+ /// <param name="tunerId">Tuner id.</param>
+ /// <response code="204">Tuner reset.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Tuners/{tunerId}/Reset")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
+ {
+ await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Gets the live tv timers.
- /// </summary>
- /// <param name="channelId">Optional. Filter by channel id.</param>
- /// <param name="seriesTimerId">Optional. Filter by timers belonging to a series timer.</param>
- /// <param name="isActive">Optional. Filter by timers that are active.</param>
- /// <param name="isScheduled">Optional. Filter by timers that are scheduled.</param>
- /// <returns>
- /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the live tv timers.
- /// </returns>
- [HttpGet("Timers")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
- [FromQuery] string? channelId,
- [FromQuery] string? seriesTimerId,
- [FromQuery] bool? isActive,
- [FromQuery] bool? isScheduled)
- {
- return await _liveTvManager.GetTimers(
- new TimerQuery
- {
- ChannelId = channelId,
- SeriesTimerId = seriesTimerId,
- IsActive = isActive,
- IsScheduled = isScheduled
- },
- CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets a timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="200">Timer returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the timer.
+ /// </returns>
+ [HttpGet("Timers/{timerId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
+ {
+ return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets available live tv epgs.
- /// </summary>
- /// <param name="channelIds">The channels to return guide information for.</param>
- /// <param name="userId">Optional. Filter by user id.</param>
- /// <param name="minStartDate">Optional. The minimum premiere start date.</param>
- /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
- /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
- /// <param name="maxStartDate">Optional. The maximum premiere start date.</param>
- /// <param name="minEndDate">Optional. The minimum premiere end date.</param>
- /// <param name="maxEndDate">Optional. The maximum premiere end date.</param>
- /// <param name="isMovie">Optional. Filter for movies.</param>
- /// <param name="isSeries">Optional. Filter for series.</param>
- /// <param name="isNews">Optional. Filter for news.</param>
- /// <param name="isKids">Optional. Filter for kids.</param>
- /// <param name="isSports">Optional. Filter for sports.</param>
- /// <param name="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="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate.</param>
- /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
- /// <param name="genres">The genres to return guide information for.</param>
- /// <param name="genreIds">The genre ids to return guide information for.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
- /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
- /// <response code="200">Live tv epgs returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
- /// </returns>
- [HttpGet("Programs")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
- [FromQuery] Guid? userId,
- [FromQuery] DateTime? minStartDate,
- [FromQuery] bool? hasAired,
- [FromQuery] bool? isAiring,
- [FromQuery] DateTime? maxStartDate,
- [FromQuery] DateTime? minEndDate,
- [FromQuery] DateTime? maxEndDate,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [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,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] string? seriesTimerId,
- [FromQuery] Guid? librarySeriesId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ /// <summary>
+ /// Gets the default values for a new timer.
+ /// </summary>
+ /// <param name="programId">Optional. To attach default values based on a program.</param>
+ /// <response code="200">Default values returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the default values for a timer.
+ /// </returns>
+ [HttpGet("Timers/Defaults")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
+ {
+ return string.IsNullOrEmpty(programId)
+ ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false)
+ : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false);
+ }
- var query = new InternalItemsQuery(user)
+ /// <summary>
+ /// Gets the live tv timers.
+ /// </summary>
+ /// <param name="channelId">Optional. Filter by channel id.</param>
+ /// <param name="seriesTimerId">Optional. Filter by timers belonging to a series timer.</param>
+ /// <param name="isActive">Optional. Filter by timers that are active.</param>
+ /// <param name="isScheduled">Optional. Filter by timers that are scheduled.</param>
+ /// <returns>
+ /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the live tv timers.
+ /// </returns>
+ [HttpGet("Timers")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
+ [FromQuery] string? channelId,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] bool? isActive,
+ [FromQuery] bool? isScheduled)
+ {
+ return await _liveTvManager.GetTimers(
+ new TimerQuery
{
- ChannelIds = channelIds,
- HasAired = hasAired,
- IsAiring = isAiring,
- EnableTotalRecordCount = enableTotalRecordCount,
- MinStartDate = minStartDate,
- MinEndDate = minEndDate,
- MaxStartDate = maxStartDate,
- MaxEndDate = maxEndDate,
- StartIndex = startIndex,
- Limit = limit,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
- IsNews = isNews,
- IsMovie = isMovie,
- IsSeries = isSeries,
- IsKids = isKids,
- IsSports = isSports,
+ ChannelId = channelId,
SeriesTimerId = seriesTimerId,
- Genres = genres,
- GenreIds = genreIds
- };
-
- if (librarySeriesId.HasValue && !librarySeriesId.Equals(default))
- {
- query.IsSeries = true;
-
- if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
- {
- query.Name = series.Name;
- }
- }
+ IsActive = isActive,
+ IsScheduled = isScheduled
+ },
+ CancellationToken.None).ConfigureAwait(false);
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets available live tv epgs.
+ /// </summary>
+ /// <param name="channelIds">The channels to return guide information for.</param>
+ /// <param name="userId">Optional. Filter by user id.</param>
+ /// <param name="minStartDate">Optional. The minimum premiere start date.</param>
+ /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
+ /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
+ /// <param name="maxStartDate">Optional. The maximum premiere start date.</param>
+ /// <param name="minEndDate">Optional. The minimum premiere end date.</param>
+ /// <param name="maxEndDate">Optional. The maximum premiere end date.</param>
+ /// <param name="isMovie">Optional. Filter for movies.</param>
+ /// <param name="isSeries">Optional. Filter for series.</param>
+ /// <param name="isNews">Optional. Filter for news.</param>
+ /// <param name="isKids">Optional. Filter for kids.</param>
+ /// <param name="isSports">Optional. Filter for sports.</param>
+ /// <param name="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="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="genres">The genres to return guide information for.</param>
+ /// <param name="genreIds">The genre ids to return guide information for.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
+ /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
+ /// <response code="200">Live tv epgs returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
+ /// </returns>
+ [HttpGet("Programs")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] DateTime? minStartDate,
+ [FromQuery] bool? hasAired,
+ [FromQuery] bool? isAiring,
+ [FromQuery] DateTime? maxStartDate,
+ [FromQuery] DateTime? minEndDate,
+ [FromQuery] DateTime? maxEndDate,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [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,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] Guid? librarySeriesId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <summary>
- /// Gets available live tv epgs.
- /// </summary>
- /// <param name="body">Request body.</param>
- /// <response code="200">Live tv epgs returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
- /// </returns>
- [HttpPost("Programs")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
+ var query = new InternalItemsQuery(user)
{
- var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
+ ChannelIds = channelIds,
+ HasAired = hasAired,
+ IsAiring = isAiring,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ MinStartDate = minStartDate,
+ MinEndDate = minEndDate,
+ MaxStartDate = maxStartDate,
+ MaxEndDate = maxEndDate,
+ StartIndex = startIndex,
+ Limit = limit,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+ IsNews = isNews,
+ IsMovie = isMovie,
+ IsSeries = isSeries,
+ IsKids = isKids,
+ IsSports = isSports,
+ SeriesTimerId = seriesTimerId,
+ Genres = genres,
+ GenreIds = genreIds
+ };
+
+ if (librarySeriesId.HasValue && !librarySeriesId.Equals(default))
+ {
+ query.IsSeries = true;
- var query = new InternalItemsQuery(user)
- {
- ChannelIds = body.ChannelIds,
- HasAired = body.HasAired,
- IsAiring = body.IsAiring,
- EnableTotalRecordCount = body.EnableTotalRecordCount,
- MinStartDate = body.MinStartDate,
- MinEndDate = body.MinEndDate,
- MaxStartDate = body.MaxStartDate,
- MaxEndDate = body.MaxEndDate,
- StartIndex = body.StartIndex,
- Limit = body.Limit,
- OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder),
- IsNews = body.IsNews,
- IsMovie = body.IsMovie,
- IsSeries = body.IsSeries,
- IsKids = body.IsKids,
- IsSports = body.IsSports,
- SeriesTimerId = body.SeriesTimerId,
- Genres = body.Genres,
- GenreIds = body.GenreIds
- };
-
- if (!body.LibrarySeriesId.Equals(default))
+ if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
{
- query.IsSeries = true;
-
- if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
- {
- query.Name = series.Name;
- }
+ query.Name = series.Name;
}
-
- var dtoOptions = new DtoOptions { Fields = body.Fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
- return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
}
- /// <summary>
- /// Gets recommended live tv epgs.
- /// </summary>
- /// <param name="userId">Optional. filter by user id.</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>
- /// <param name="isSeries">Optional. Filter for series.</param>
- /// <param name="isMovie">Optional. Filter for movies.</param>
- /// <param name="isNews">Optional. Filter for news.</param>
- /// <param name="isKids">Optional. Filter for kids.</param>
- /// <param name="isSports">Optional. Filter for sports.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <param name="genreIds">The genres to return guide information for.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. include user data.</param>
- /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
- /// <response code="200">Recommended epgs returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
- [HttpGet("Programs/Recommended")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
- [FromQuery] Guid? userId,
- [FromQuery] int? limit,
- [FromQuery] bool? isAiring,
- [FromQuery] bool? hasAired,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var query = new InternalItemsQuery(user)
- {
- IsAiring = isAiring,
- Limit = limit,
- HasAired = hasAired,
- IsSeries = isSeries,
- IsMovie = isMovie,
- IsKids = isKids,
- IsNews = isNews,
- IsSports = isSports,
- EnableTotalRecordCount = enableTotalRecordCount,
- GenreIds = genreIds
- };
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets available live tv epgs.
+ /// </summary>
+ /// <param name="body">Request body.</param>
+ /// <response code="200">Live tv epgs returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
+ /// </returns>
+ [HttpPost("Programs")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
+ {
+ var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
- /// <summary>
- /// Gets a live tv program.
- /// </summary>
- /// <param name="programId">Program id.</param>
- /// <param name="userId">Optional. Attach user data.</param>
- /// <response code="200">Program returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
- [HttpGet("Programs/{programId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<BaseItemDto>> GetProgram(
- [FromRoute, Required] string programId,
- [FromQuery] Guid? userId)
+ var query = new InternalItemsQuery(user)
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Deletes a live tv recording.
- /// </summary>
- /// <param name="recordingId">Recording id.</param>
- /// <response code="204">Recording deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpDelete("Recordings/{recordingId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
+ ChannelIds = body.ChannelIds,
+ HasAired = body.HasAired,
+ IsAiring = body.IsAiring,
+ EnableTotalRecordCount = body.EnableTotalRecordCount,
+ MinStartDate = body.MinStartDate,
+ MinEndDate = body.MinEndDate,
+ MaxStartDate = body.MaxStartDate,
+ MaxEndDate = body.MaxEndDate,
+ StartIndex = body.StartIndex,
+ Limit = body.Limit,
+ OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder),
+ IsNews = body.IsNews,
+ IsMovie = body.IsMovie,
+ IsSeries = body.IsSeries,
+ IsKids = body.IsKids,
+ IsSports = body.IsSports,
+ SeriesTimerId = body.SeriesTimerId,
+ Genres = body.Genres,
+ GenreIds = body.GenreIds
+ };
+
+ if (!body.LibrarySeriesId.Equals(default))
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
+ query.IsSeries = true;
- var item = _libraryManager.GetItemById(recordingId);
- if (item is null)
+ if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
{
- return NotFound();
+ query.Name = series.Name;
}
-
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- });
-
- return NoContent();
}
- /// <summary>
- /// Cancels a live tv timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="204">Timer deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Timers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
- return NoContent();
- }
+ var dtoOptions = new DtoOptions { Fields = body.Fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
+ return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Updates a live tv timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <param name="timerInfo">New timer info.</param>
- /// <response code="204">Timer updated.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Timers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Gets recommended live tv epgs.
+ /// </summary>
+ /// <param name="userId">Optional. filter by user id.</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>
+ /// <param name="isSeries">Optional. Filter for series.</param>
+ /// <param name="isMovie">Optional. Filter for movies.</param>
+ /// <param name="isNews">Optional. Filter for news.</param>
+ /// <param name="isKids">Optional. Filter for kids.</param>
+ /// <param name="isSports">Optional. Filter for sports.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <param name="genreIds">The genres to return guide information for.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. include user data.</param>
+ /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
+ /// <response code="200">Recommended epgs returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
+ [HttpGet("Programs/Recommended")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] bool? isAiring,
+ [FromQuery] bool? hasAired,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <summary>
- /// Creates a live tv timer.
- /// </summary>
- /// <param name="timerInfo">New timer info.</param>
- /// <response code="204">Timer created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Timers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
+ var query = new InternalItemsQuery(user)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ IsAiring = isAiring,
+ Limit = limit,
+ HasAired = hasAired,
+ IsSeries = isSeries,
+ IsMovie = isMovie,
+ IsKids = isKids,
+ IsNews = isNews,
+ IsSports = isSports,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ GenreIds = genreIds
+ };
+
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a live tv series timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="200">Series timer returned.</response>
- /// <response code="404">Series timer not found.</response>
- /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
- [HttpGet("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
- {
- var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
- if (timer is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Gets a live tv program.
+ /// </summary>
+ /// <param name="programId">Program id.</param>
+ /// <param name="userId">Optional. Attach user data.</param>
+ /// <response code="200">Program returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
+ [HttpGet("Programs/{programId}")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<BaseItemDto>> GetProgram(
+ [FromRoute, Required] string programId,
+ [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- return timer;
- }
+ return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets live tv series timers.
- /// </summary>
- /// <param name="sortBy">Optional. Sort by SortName or Priority.</param>
- /// <param name="sortOrder">Optional. Sort in Ascending or Descending order.</param>
- /// <response code="200">Timers returned.</response>
- /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
- [HttpGet("SeriesTimers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
+ /// <summary>
+ /// Deletes a live tv recording.
+ /// </summary>
+ /// <param name="recordingId">Recording id.</param>
+ /// <response code="204">Recording deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Recordings/{recordingId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
+ {
+ var item = _libraryManager.GetItemById(recordingId);
+ if (item is null)
{
- return await _liveTvManager.GetSeriesTimers(
- new SeriesTimerQuery
- {
- SortOrder = sortOrder ?? SortOrder.Ascending,
- SortBy = sortBy
- },
- CancellationToken.None).ConfigureAwait(false);
+ return NotFound();
}
- /// <summary>
- /// Cancels a live tv series timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="204">Timer cancelled.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
+ _libraryManager.DeleteItem(item, new DeleteOptions
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
- return NoContent();
- }
+ DeleteFileLocation = false
+ });
- /// <summary>
- /// Updates a live tv series timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <param name="seriesTimerInfo">New series timer info.</param>
- /// <response code="204">Series timer updated.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
+ }
- /// <summary>
- /// Creates a live tv series timer.
- /// </summary>
- /// <param name="seriesTimerInfo">New series timer info.</param>
- /// <response code="204">Series timer info created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("SeriesTimers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Cancels a live tv timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="204">Timer deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Timers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
+ {
+ await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Updates a live tv timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <param name="timerInfo">New timer info.</param>
+ /// <response code="204">Timer updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Timers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
+ {
+ await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Creates a live tv timer.
+ /// </summary>
+ /// <param name="timerInfo">New timer info.</param>
+ /// <response code="204">Timer created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Timers")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
+ {
+ await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Get recording group.
- /// </summary>
- /// <param name="groupId">Group id.</param>
- /// <returns>A <see cref="NotFoundResult"/>.</returns>
- [HttpGet("Recordings/Groups/{groupId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Obsolete("This endpoint is obsolete.")]
- public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
+ /// <summary>
+ /// Gets a live tv series timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="200">Series timer returned.</response>
+ /// <response code="404">Series timer not found.</response>
+ /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
+ [HttpGet("SeriesTimers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
+ {
+ var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+ if (timer is null)
{
return NotFound();
}
- /// <summary>
- /// Get guid info.
- /// </summary>
- /// <response code="200">Guid info returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
- [HttpGet("GuideInfo")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<GuideInfo> GetGuideInfo()
- {
- return _liveTvManager.GetGuideInfo();
- }
+ return timer;
+ }
- /// <summary>
- /// Adds a tuner host.
- /// </summary>
- /// <param name="tunerHostInfo">New tuner host.</param>
- /// <response code="200">Created tuner host returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
- [HttpPost("TunerHosts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
- {
- return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets live tv series timers.
+ /// </summary>
+ /// <param name="sortBy">Optional. Sort by SortName or Priority.</param>
+ /// <param name="sortOrder">Optional. Sort in Ascending or Descending order.</param>
+ /// <response code="200">Timers returned.</response>
+ /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
+ [HttpGet("SeriesTimers")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
+ {
+ return await _liveTvManager.GetSeriesTimers(
+ new SeriesTimerQuery
+ {
+ SortOrder = sortOrder ?? SortOrder.Ascending,
+ SortBy = sortBy
+ },
+ CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Deletes a tuner host.
- /// </summary>
- /// <param name="id">Tuner host id.</param>
- /// <response code="204">Tuner host deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("TunerHosts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult DeleteTunerHost([FromQuery] string? id)
- {
- var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
- config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
- _configurationManager.SaveConfiguration("livetv", config);
- return NoContent();
- }
+ /// <summary>
+ /// Cancels a live tv series timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="204">Timer cancelled.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("SeriesTimers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
+ {
+ await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Gets default listings provider info.
- /// </summary>
- /// <response code="200">Default listings provider info returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
- [HttpGet("ListingProviders/Default")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
- {
- return new ListingsProviderInfo();
- }
+ /// <summary>
+ /// Updates a live tv series timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <param name="seriesTimerInfo">New series timer info.</param>
+ /// <response code="204">Series timer updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("SeriesTimers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
+ {
+ await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Adds a listings provider.
- /// </summary>
- /// <param name="pw">Password.</param>
- /// <param name="listingsProviderInfo">New listings info.</param>
- /// <param name="validateListings">Validate listings.</param>
- /// <param name="validateLogin">Validate login.</param>
- /// <response code="200">Created listings provider returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
- [HttpPost("ListingProviders")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
- [FromQuery] string? pw,
- [FromBody] ListingsProviderInfo listingsProviderInfo,
- [FromQuery] bool validateListings = false,
- [FromQuery] bool validateLogin = false)
- {
- if (!string.IsNullOrEmpty(pw))
- {
- // TODO: remove ToLower when Convert.ToHexString supports lowercase
- // Schedules Direct requires the hex to be lowercase
- listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
- }
+ /// <summary>
+ /// Creates a live tv series timer.
+ /// </summary>
+ /// <param name="seriesTimerInfo">New series timer info.</param>
+ /// <response code="204">Series timer info created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("SeriesTimers")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
+ {
+ await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Get recording group.
+ /// </summary>
+ /// <param name="groupId">Group id.</param>
+ /// <returns>A <see cref="NotFoundResult"/>.</returns>
+ [HttpGet("Recordings/Groups/{groupId}")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("This endpoint is obsolete.")]
+ public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
+ {
+ return NotFound();
+ }
- /// <summary>
- /// Delete listing provider.
- /// </summary>
- /// <param name="id">Listing provider id.</param>
- /// <response code="204">Listing provider deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("ListingProviders")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult DeleteListingProvider([FromQuery] string? id)
- {
- _liveTvManager.DeleteListingsProvider(id);
- return NoContent();
- }
+ /// <summary>
+ /// Get guid info.
+ /// </summary>
+ /// <response code="200">Guid info returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
+ [HttpGet("GuideInfo")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<GuideInfo> GetGuideInfo()
+ {
+ return _liveTvManager.GetGuideInfo();
+ }
+
+ /// <summary>
+ /// Adds a tuner host.
+ /// </summary>
+ /// <param name="tunerHostInfo">New tuner host.</param>
+ /// <response code="200">Created tuner host returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
+ [HttpPost("TunerHosts")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
+ {
+ return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Deletes a tuner host.
+ /// </summary>
+ /// <param name="id">Tuner host id.</param>
+ /// <response code="204">Tuner host deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("TunerHosts")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult DeleteTunerHost([FromQuery] string? id)
+ {
+ var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+ config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
+ _configurationManager.SaveConfiguration("livetv", config);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets default listings provider info.
+ /// </summary>
+ /// <response code="200">Default listings provider info returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
+ [HttpGet("ListingProviders/Default")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
+ {
+ return new ListingsProviderInfo();
+ }
- /// <summary>
- /// Gets available lineups.
- /// </summary>
- /// <param name="id">Provider id.</param>
- /// <param name="type">Provider type.</param>
- /// <param name="location">Location.</param>
- /// <param name="country">Country.</param>
- /// <response code="200">Available lineups returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
- [HttpGet("ListingProviders/Lineups")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
- [FromQuery] string? id,
- [FromQuery] string? type,
- [FromQuery] string? location,
- [FromQuery] string? country)
+ /// <summary>
+ /// Adds a listings provider.
+ /// </summary>
+ /// <param name="pw">Password.</param>
+ /// <param name="listingsProviderInfo">New listings info.</param>
+ /// <param name="validateListings">Validate listings.</param>
+ /// <param name="validateLogin">Validate login.</param>
+ /// <response code="200">Created listings provider returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
+ [HttpPost("ListingProviders")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
+ [FromQuery] string? pw,
+ [FromBody] ListingsProviderInfo listingsProviderInfo,
+ [FromQuery] bool validateListings = false,
+ [FromQuery] bool validateLogin = false)
+ {
+ if (!string.IsNullOrEmpty(pw))
{
- return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
+ // TODO: remove ToLower when Convert.ToHexString supports lowercase
+ // Schedules Direct requires the hex to be lowercase
+ listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
}
- /// <summary>
- /// Gets available countries.
- /// </summary>
- /// <response code="200">Available countries returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
- [HttpGet("ListingProviders/SchedulesDirect/Countries")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile(MediaTypeNames.Application.Json)]
- public async Task<ActionResult> GetSchedulesDirectCountries()
- {
- var client = _httpClientFactory.CreateClient(NamedClient.Default);
- // https://json.schedulesdirect.org/20141201/available/countries
- // Can't dispose the response as it's required up the call chain.
- var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
- .ConfigureAwait(false);
+ return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
+ }
- return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
- }
+ /// <summary>
+ /// Delete listing provider.
+ /// </summary>
+ /// <param name="id">Listing provider id.</param>
+ /// <response code="204">Listing provider deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("ListingProviders")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult DeleteListingProvider([FromQuery] string? id)
+ {
+ _liveTvManager.DeleteListingsProvider(id);
+ return NoContent();
+ }
- /// <summary>
- /// Get channel mapping options.
- /// </summary>
- /// <param name="providerId">Provider id.</param>
- /// <response code="200">Channel mapping options returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
- [HttpGet("ChannelMappingOptions")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
- {
- var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+ /// <summary>
+ /// Gets available lineups.
+ /// </summary>
+ /// <param name="id">Provider id.</param>
+ /// <param name="type">Provider type.</param>
+ /// <param name="location">Location.</param>
+ /// <param name="country">Country.</param>
+ /// <response code="200">Available lineups returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
+ [HttpGet("ListingProviders/Lineups")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
+ [FromQuery] string? id,
+ [FromQuery] string? type,
+ [FromQuery] string? location,
+ [FromQuery] string? country)
+ {
+ return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
+ }
- var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
+ /// <summary>
+ /// Gets available countries.
+ /// </summary>
+ /// <response code="200">Available countries returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
+ [HttpGet("ListingProviders/SchedulesDirect/Countries")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Application.Json)]
+ public async Task<ActionResult> GetSchedulesDirectCountries()
+ {
+ var client = _httpClientFactory.CreateClient(NamedClient.Default);
+ // https://json.schedulesdirect.org/20141201/available/countries
+ // Can't dispose the response as it's required up the call chain.
+ var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
+ .ConfigureAwait(false);
- var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
+ return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
+ }
- var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get channel mapping options.
+ /// </summary>
+ /// <param name="providerId">Provider id.</param>
+ /// <response code="200">Channel mapping options returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
+ [HttpGet("ChannelMappingOptions")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
+ {
+ var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
- var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
- .ConfigureAwait(false);
+ var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
- var mappings = listingsProviderInfo.ChannelMappings;
+ var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
- return new ChannelMappingOptionsDto
- {
- TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
- ProviderChannels = providerChannels.Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = i.Id
- }).ToList(),
- Mappings = mappings,
- ProviderName = listingsProviderName
- };
- }
+ var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
- /// <summary>
- /// Set channel mappings.
- /// </summary>
- /// <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([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
- {
- return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
- }
+ var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
- /// <summary>
- /// Get tuner host types.
- /// </summary>
- /// <response code="200">Tuner host types returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
- [HttpGet("TunerHosts/Types")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
- {
- return _liveTvManager.GetTunerHostTypes();
- }
+ var mappings = listingsProviderInfo.ChannelMappings;
- /// <summary>
- /// Discover tuners.
- /// </summary>
- /// <param name="newDevicesOnly">Only discover new tuners.</param>
- /// <response code="200">Tuners returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
- [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
- [HttpGet("Tuners/Discover")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
+ return new ChannelMappingOptionsDto
{
- return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
- }
+ TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
+ ProviderChannels = providerChannels.Select(i => new NameIdPair
+ {
+ Name = i.Name,
+ Id = i.Id
+ }).ToList(),
+ Mappings = mappings,
+ ProviderName = listingsProviderName
+ };
+ }
- /// <summary>
- /// Gets a live tv recording stream.
- /// </summary>
- /// <param name="recordingId">Recording id.</param>
- /// <response code="200">Recording stream returned.</response>
- /// <response code="404">Recording not found.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the recording stream on success,
- /// or a <see cref="NotFoundResult"/> if recording not found.
- /// </returns>
- [HttpGet("LiveRecordings/{recordingId}/stream")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesVideoFile]
- public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
- {
- var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
+ /// <summary>
+ /// Set channel mappings.
+ /// </summary>
+ /// <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.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
+ {
+ return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
+ }
- if (string.IsNullOrWhiteSpace(path))
- {
- return NotFound();
- }
+ /// <summary>
+ /// Get tuner host types.
+ /// </summary>
+ /// <response code="200">Tuner host types returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
+ [HttpGet("TunerHosts/Types")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
+ {
+ return _liveTvManager.GetTunerHostTypes();
+ }
- var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
- return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
- }
+ /// <summary>
+ /// Discover tuners.
+ /// </summary>
+ /// <param name="newDevicesOnly">Only discover new tuners.</param>
+ /// <response code="200">Tuners returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
+ [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
+ [HttpGet("Tuners/Discover")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
+ {
+ return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a live tv channel stream.
- /// </summary>
- /// <param name="streamId">Stream id.</param>
- /// <param name="container">Container type.</param>
- /// <response code="200">Stream returned.</response>
- /// <response code="404">Stream not found.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the channel stream on success,
- /// or a <see cref="NotFoundResult"/> if stream not found.
- /// </returns>
- [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesVideoFile]
- public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
- {
- var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
- if (liveStreamInfo is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Gets a live tv recording stream.
+ /// </summary>
+ /// <param name="recordingId">Recording id.</param>
+ /// <response code="200">Recording stream returned.</response>
+ /// <response code="404">Recording not found.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the recording stream on success,
+ /// or a <see cref="NotFoundResult"/> if recording not found.
+ /// </returns>
+ [HttpGet("LiveRecordings/{recordingId}/stream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesVideoFile]
+ public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
+ {
+ var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
- return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return NotFound();
}
- private async Task AssertUserCanManageLiveTv()
- {
- var user = _userManager.GetUserById(User.GetUserId());
- var session = await _sessionManager.LogSessionActivity(
- User.GetClient(),
- User.GetVersion(),
- User.GetDeviceId(),
- User.GetDevice(),
- HttpContext.GetNormalizedRemoteIp().ToString(),
- user).ConfigureAwait(false);
-
- if (session.UserId.Equals(default))
- {
- throw new SecurityException("Anonymous live tv management is not allowed.");
- }
+ var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
+ }
- if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
- {
- throw new SecurityException("The current user does not have permission to manage live tv.");
- }
+ /// <summary>
+ /// Gets a live tv channel stream.
+ /// </summary>
+ /// <param name="streamId">Stream id.</param>
+ /// <param name="container">Container type.</param>
+ /// <response code="200">Stream returned.</response>
+ /// <response code="404">Stream not found.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the channel stream on success,
+ /// or a <see cref="NotFoundResult"/> if stream not found.
+ /// </returns>
+ [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesVideoFile]
+ public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
+ {
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
+ if (liveStreamInfo is null)
+ {
+ return NotFound();
}
+
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
}
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index 3d8b9e0ca..b9772a069 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -6,71 +6,70 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Localization controller.
+/// </summary>
+[Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
+public class LocalizationController : BaseJellyfinApiController
{
+ private readonly ILocalizationManager _localization;
+
/// <summary>
- /// Localization controller.
+ /// Initializes a new instance of the <see cref="LocalizationController"/> class.
/// </summary>
- [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
- public class LocalizationController : BaseJellyfinApiController
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public LocalizationController(ILocalizationManager localization)
{
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LocalizationController"/> class.
- /// </summary>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public LocalizationController(ILocalizationManager localization)
- {
- _localization = localization;
- }
+ _localization = localization;
+ }
- /// <summary>
- /// Gets known cultures.
- /// </summary>
- /// <response code="200">Known cultures returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
- [HttpGet("Cultures")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<CultureDto>> GetCultures()
- {
- return Ok(_localization.GetCultures());
- }
+ /// <summary>
+ /// Gets known cultures.
+ /// </summary>
+ /// <response code="200">Known cultures returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
+ [HttpGet("Cultures")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<CultureDto>> GetCultures()
+ {
+ return Ok(_localization.GetCultures());
+ }
- /// <summary>
- /// Gets known countries.
- /// </summary>
- /// <response code="200">Known countries returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
- [HttpGet("Countries")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<CountryInfo>> GetCountries()
- {
- return Ok(_localization.GetCountries());
- }
+ /// <summary>
+ /// Gets known countries.
+ /// </summary>
+ /// <response code="200">Known countries returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
+ [HttpGet("Countries")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<CountryInfo>> GetCountries()
+ {
+ return Ok(_localization.GetCountries());
+ }
- /// <summary>
- /// Gets known parental ratings.
- /// </summary>
- /// <response code="200">Known parental ratings returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
- [HttpGet("ParentalRatings")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
- {
- return Ok(_localization.GetParentalRatings());
- }
+ /// <summary>
+ /// Gets known parental ratings.
+ /// </summary>
+ /// <response code="200">Known parental ratings returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
+ [HttpGet("ParentalRatings")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
+ {
+ return Ok(_localization.GetParentalRatings());
+ }
- /// <summary>
- /// Gets localization options.
- /// </summary>
- /// <response code="200">Localization options returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
- [HttpGet("Options")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
- {
- return Ok(_localization.GetLocalizationOptions());
- }
+ /// <summary>
+ /// Gets localization options.
+ /// </summary>
+ /// <response code="200">Localization options returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
+ [HttpGet("Options")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
+ {
+ return Ok(_localization.GetLocalizationOptions());
}
}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 8115c3585..da24616ff 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
@@ -19,295 +18,297 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The media info controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class MediaInfoController : BaseJellyfinApiController
{
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IDeviceManager _deviceManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MediaInfoController> _logger;
+ private readonly MediaInfoHelper _mediaInfoHelper;
+
/// <summary>
- /// The media info controller.
+ /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class MediaInfoController : BaseJellyfinApiController
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
+ /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
+ public MediaInfoController(
+ IMediaSourceManager mediaSourceManager,
+ IDeviceManager deviceManager,
+ ILibraryManager libraryManager,
+ ILogger<MediaInfoController> logger,
+ MediaInfoHelper mediaInfoHelper)
{
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IDeviceManager _deviceManager;
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<MediaInfoController> _logger;
- private readonly MediaInfoHelper _mediaInfoHelper;
+ _mediaSourceManager = mediaSourceManager;
+ _deviceManager = deviceManager;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _mediaInfoHelper = mediaInfoHelper;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
- /// </summary>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
- /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
- public MediaInfoController(
- IMediaSourceManager mediaSourceManager,
- IDeviceManager deviceManager,
- ILibraryManager libraryManager,
- ILogger<MediaInfoController> logger,
- MediaInfoHelper mediaInfoHelper)
- {
- _mediaSourceManager = mediaSourceManager;
- _deviceManager = deviceManager;
- _libraryManager = libraryManager;
- _logger = logger;
- _mediaInfoHelper = mediaInfoHelper;
- }
+ /// <summary>
+ /// Gets live playback media info for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">Playback info returned.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
+ [HttpGet("Items/{itemId}/PlaybackInfo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
+ {
+ return await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId)
+ .ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets live playback media info for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">The user id.</param>
- /// <response code="200">Playback info returned.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
- [HttpGet("Items/{itemId}/PlaybackInfo")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
- {
- return await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId)
- .ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets live playback media info for an item.
+ /// </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>
+ /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ /// <param name="audioStreamIndex">The audio stream index.</param>
+ /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+ /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <param name="liveStreamId">The livestream id.</param>
+ /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
+ /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
+ /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
+ /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
+ /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
+ /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
+ /// <param name="playbackInfoDto">The playback info.</param>
+ /// <response code="200">Playback info returned.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
+ [HttpPost("Items/{itemId}/PlaybackInfo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
+ [FromRoute, Required] Guid itemId,
+ [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 profile = playbackInfoDto?.DeviceProfile;
+ _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile);
- /// <summary>
- /// Gets live playback media info for an item.
- /// </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>
- /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- /// <param name="audioStreamIndex">The audio stream index.</param>
- /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
- /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
- /// <param name="mediaSourceId">The media source id.</param>
- /// <param name="liveStreamId">The livestream id.</param>
- /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
- /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
- /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
- /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
- /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
- /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
- /// <param name="playbackInfoDto">The playback info.</param>
- /// <response code="200">Playback info returned.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
- [HttpPost("Items/{itemId}/PlaybackInfo")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
- [FromRoute, Required] Guid itemId,
- [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)
+ if (profile is null)
{
- var profile = playbackInfoDto?.DeviceProfile;
- _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile);
-
- if (profile is null)
+ var caps = _deviceManager.GetCapabilities(User.GetDeviceId());
+ if (caps is not null)
{
- var caps = _deviceManager.GetCapabilities(User.GetDeviceId());
- if (caps is not null)
- {
- profile = caps.DeviceProfile;
- }
+ profile = caps.DeviceProfile;
}
+ }
- // Copy params from posted body
- // TODO clean up when breaking API compatibility.
- userId ??= playbackInfoDto?.UserId;
- maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
- startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
- audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
- subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex;
- maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels;
- mediaSourceId ??= playbackInfoDto?.MediaSourceId;
- liveStreamId ??= playbackInfoDto?.LiveStreamId;
- autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false;
- enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true;
- enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true;
- enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true;
- allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
- allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
+ // Copy params from posted body
+ // TODO clean up when breaking API compatibility.
+ userId ??= playbackInfoDto?.UserId;
+ userId = RequestHelpers.GetUserId(User, userId);
+ maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
+ startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
+ audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
+ subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex;
+ maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels;
+ mediaSourceId ??= playbackInfoDto?.MediaSourceId;
+ liveStreamId ??= playbackInfoDto?.LiveStreamId;
+ autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false;
+ enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true;
+ enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true;
+ enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true;
+ allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
+ allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
- var info = await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId,
- mediaSourceId,
- liveStreamId)
- .ConfigureAwait(false);
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId,
+ liveStreamId)
+ .ConfigureAwait(false);
- if (info.ErrorCode is not null)
- {
- return info;
- }
+ if (info.ErrorCode is not null)
+ {
+ return info;
+ }
+
+ if (profile is not null)
+ {
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
- if (profile is not null)
+ foreach (var mediaSource in info.MediaSources)
{
- // set device specific data
- var item = _libraryManager.GetItemById(itemId);
+ _mediaInfoHelper.SetDeviceSpecificData(
+ item,
+ mediaSource,
+ profile,
+ User,
+ maxStreamingBitrate ?? profile.MaxStreamingBitrate,
+ startTimeTicks ?? 0,
+ mediaSourceId ?? string.Empty,
+ audioStreamIndex,
+ subtitleStreamIndex,
+ maxAudioChannels,
+ info.PlaySessionId!,
+ userId ?? Guid.Empty,
+ enableDirectPlay.Value,
+ enableDirectStream.Value,
+ enableTranscoding.Value,
+ allowVideoStreamCopy.Value,
+ allowAudioStreamCopy.Value,
+ Request.HttpContext.GetNormalizedRemoteIp());
+ }
- foreach (var mediaSource in info.MediaSources)
- {
- _mediaInfoHelper.SetDeviceSpecificData(
- item,
- mediaSource,
- profile,
- User,
- maxStreamingBitrate ?? profile.MaxStreamingBitrate,
- startTimeTicks ?? 0,
- mediaSourceId ?? string.Empty,
- audioStreamIndex,
- subtitleStreamIndex,
- maxAudioChannels,
- info.PlaySessionId!,
- userId ?? Guid.Empty,
- enableDirectPlay.Value,
- enableDirectStream.Value,
- enableTranscoding.Value,
- allowVideoStreamCopy.Value,
- allowAudioStreamCopy.Value,
- Request.HttpContext.GetNormalizedRemoteIp());
- }
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+ }
- _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
- }
+ if (autoOpenLiveStream.Value)
+ {
+ var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
- if (autoOpenLiveStream.Value)
+ if (mediaSource is not null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
{
- var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
-
- if (mediaSource is not null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
- {
- var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
- HttpContext,
- new LiveStreamRequest
- {
- AudioStreamIndex = audioStreamIndex,
- DeviceProfile = playbackInfoDto?.DeviceProfile,
- EnableDirectPlay = enableDirectPlay.Value,
- EnableDirectStream = enableDirectStream.Value,
- ItemId = itemId,
- MaxAudioChannels = maxAudioChannels,
- MaxStreamingBitrate = maxStreamingBitrate,
- PlaySessionId = info.PlaySessionId,
- StartTimeTicks = startTimeTicks,
- SubtitleStreamIndex = subtitleStreamIndex,
- UserId = userId ?? Guid.Empty,
- OpenToken = mediaSource.OpenToken
- }).ConfigureAwait(false);
+ var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
+ HttpContext,
+ new LiveStreamRequest
+ {
+ AudioStreamIndex = audioStreamIndex,
+ DeviceProfile = playbackInfoDto?.DeviceProfile,
+ EnableDirectPlay = enableDirectPlay.Value,
+ EnableDirectStream = enableDirectStream.Value,
+ ItemId = itemId,
+ MaxAudioChannels = maxAudioChannels,
+ MaxStreamingBitrate = maxStreamingBitrate,
+ PlaySessionId = info.PlaySessionId,
+ StartTimeTicks = startTimeTicks,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ UserId = userId ?? Guid.Empty,
+ OpenToken = mediaSource.OpenToken
+ }).ConfigureAwait(false);
- info.MediaSources = new[] { openStreamResult.MediaSource };
- }
+ info.MediaSources = new[] { openStreamResult.MediaSource };
}
-
- return info;
}
- /// <summary>
- /// Opens a media source.
- /// </summary>
- /// <param name="openToken">The open token.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- /// <param name="audioStreamIndex">The audio stream index.</param>
- /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
- /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
- /// <param name="itemId">The item id.</param>
- /// <param name="openLiveStreamDto">The open live stream dto.</param>
- /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
- /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
- /// <response code="200">Media source opened.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
- [HttpPost("LiveStreams/Open")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
- [FromQuery] string? openToken,
- [FromQuery] Guid? userId,
- [FromQuery] string? playSessionId,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] Guid? itemId,
- [FromBody] OpenLiveStreamDto? openLiveStreamDto,
- [FromQuery] bool? enableDirectPlay,
- [FromQuery] bool? enableDirectStream)
+ return info;
+ }
+
+ /// <summary>
+ /// Opens a media source.
+ /// </summary>
+ /// <param name="openToken">The open token.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ /// <param name="audioStreamIndex">The audio stream index.</param>
+ /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+ /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="openLiveStreamDto">The open live stream dto.</param>
+ /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
+ /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
+ /// <response code="200">Media source opened.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
+ [HttpPost("LiveStreams/Open")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
+ [FromQuery] string? openToken,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] Guid? itemId,
+ [FromBody] OpenLiveStreamDto? openLiveStreamDto,
+ [FromQuery] bool? enableDirectPlay,
+ [FromQuery] bool? enableDirectStream)
+ {
+ userId ??= openLiveStreamDto?.UserId;
+ userId = RequestHelpers.GetUserId(User, userId);
+ var request = new LiveStreamRequest
{
- var request = new LiveStreamRequest
- {
- 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 ?? openLiveStreamDto?.EnableDirectPlay ?? true,
- EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
- DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
- };
- return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false);
- }
+ OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
+ UserId = userId.Value,
+ 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 ?? openLiveStreamDto?.EnableDirectPlay ?? true,
+ EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
+ DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
+ };
+ return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false);
+ }
- /// <summary>
- /// Closes a media source.
- /// </summary>
- /// <param name="liveStreamId">The livestream id.</param>
- /// <response code="204">Livestream closed.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("LiveStreams/Close")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string liveStreamId)
+ /// <summary>
+ /// Closes a media source.
+ /// </summary>
+ /// <param name="liveStreamId">The livestream id.</param>
+ /// <response code="204">Livestream closed.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("LiveStreams/Close")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string liveStreamId)
+ {
+ await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Tests the network with a request with the size of the bitrate.
+ /// </summary>
+ /// <param name="size">The bitrate. Defaults to 102400.</param>
+ /// <response code="200">Test buffer returned.</response>
+ /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
+ [HttpGet("Playback/BitrateTest")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Application.Octet)]
+ public ActionResult GetBitrateTestBytes([FromQuery][Range(1, 100_000_000, ErrorMessage = "The requested size must be greater than or equal to {1} and less than or equal to {2}")] int size = 102400)
+ {
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
+ try
{
- await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
- return NoContent();
+ Random.Shared.NextBytes(buffer);
+ return File(buffer, MediaTypeNames.Application.Octet);
}
-
- /// <summary>
- /// Tests the network with a request with the size of the bitrate.
- /// </summary>
- /// <param name="size">The bitrate. Defaults to 102400.</param>
- /// <response code="200">Test buffer returned.</response>
- /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
- [HttpGet("Playback/BitrateTest")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile(MediaTypeNames.Application.Octet)]
- public ActionResult GetBitrateTestBytes([FromQuery][Range(1, 100_000_000, ErrorMessage = "The requested size must be greater than or equal to {1} and less than or equal to {2}")] int size = 102400)
+ finally
{
- byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
- try
- {
- Random.Shared.NextBytes(buffer);
- return File(buffer, MediaTypeNames.Application.Octet);
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(buffer);
- }
+ ArrayPool<byte>.Shared.Return(buffer);
}
}
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 3cf079362..e1145481f 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -2,8 +2,8 @@ using System;
using System.Collections.Generic;
using System.Globalization;
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;
@@ -18,122 +18,123 @@ using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Movies controller.
+/// </summary>
+[Authorize]
+public class MoviesController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Movies controller.
+ /// Initializes a new instance of the <see cref="MoviesController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class MoviesController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public MoviesController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IServerConfigurationManager serverConfigurationManager)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MoviesController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public MoviesController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IServerConfigurationManager serverConfigurationManager)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _serverConfigurationManager = serverConfigurationManager;
- }
-
- /// <summary>
- /// Gets movie recommendations.
- /// </summary>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. The fields to return.</param>
- /// <param name="categoryLimit">The max number of categories to return.</param>
- /// <param name="itemLimit">The max number of items to return per category.</param>
- /// <response code="200">Movie recommendations returned.</response>
- /// <returns>The list of movie recommendations.</returns>
- [HttpGet("Recommendations")]
- public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
- [FromQuery] Guid? userId,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] int categoryLimit = 5,
- [FromQuery] int itemLimit = 8)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User);
-
- var categories = new List<RecommendationDto>();
-
- var parentIdGuid = parentId ?? Guid.Empty;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- var query = new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[]
- {
- BaseItemKind.Movie,
- // nameof(Trailer),
- // nameof(LiveTvProgram)
- },
- // IsMovie = true
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
- Limit = 7,
- ParentId = parentIdGuid,
- Recursive = true,
- IsPlayed = true,
- DtoOptions = dtoOptions
- };
+ /// <summary>
+ /// Gets movie recommendations.
+ /// </summary>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. The fields to return.</param>
+ /// <param name="categoryLimit">The max number of categories to return.</param>
+ /// <param name="itemLimit">The max number of items to return per category.</param>
+ /// <response code="200">Movie recommendations returned.</response>
+ /// <returns>The list of movie recommendations.</returns>
+ [HttpGet("Recommendations")]
+ public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
+ [FromQuery] Guid? userId,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] int categoryLimit = 5,
+ [FromQuery] int itemLimit = 8)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User);
- var recentlyPlayedMovies = _libraryManager.GetItemList(query);
+ var categories = new List<RecommendationDto>();
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
+ var parentIdGuid = parentId ?? Guid.Empty;
- var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var query = new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[]
{
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
- Limit = 10,
- IsFavoriteOrLiked = true,
- ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
- EnableGroupByMetadataKey = true,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = dtoOptions
- });
-
- var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
- // Get recently played directors
- var recentDirectors = GetDirectors(mostRecentMovies)
- .ToList();
-
- // Get recently played actors
- var recentActors = GetActors(mostRecentMovies)
- .ToList();
-
- var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
- var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
-
- var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
- var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
+ BaseItemKind.Movie,
+ // nameof(Trailer),
+ // nameof(LiveTvProgram)
+ },
+ // IsMovie = true
+ OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
+ Limit = 7,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ IsPlayed = true,
+ DtoOptions = dtoOptions
+ };
+
+ var recentlyPlayedMovies = _libraryManager.GetItemList(query);
+
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
- var categoryTypes = new List<IEnumerator<RecommendationDto>>
+ var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
+ Limit = 10,
+ IsFavoriteOrLiked = true,
+ ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
+ EnableGroupByMetadataKey = true,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ DtoOptions = dtoOptions
+ });
+
+ var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
+ // Get recently played directors
+ var recentDirectors = GetDirectors(mostRecentMovies)
+ .ToList();
+
+ // Get recently played actors
+ var recentActors = GetActors(mostRecentMovies)
+ .ToList();
+
+ var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
+ var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
+
+ var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
+ var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
+
+ var categoryTypes = new List<IEnumerator<RecommendationDto>>
{
// Give this extra weight
similarToRecentlyPlayed,
@@ -146,181 +147,180 @@ namespace Jellyfin.Api.Controllers
hasActorFromRecentlyPlayed
};
- while (categories.Count < categoryLimit)
- {
- var allEmpty = true;
+ while (categories.Count < categoryLimit)
+ {
+ var allEmpty = true;
- foreach (var category in categoryTypes)
+ foreach (var category in categoryTypes)
+ {
+ if (category.MoveNext())
{
- if (category.MoveNext())
- {
- categories.Add(category.Current);
- allEmpty = false;
+ categories.Add(category.Current);
+ allEmpty = false;
- if (categories.Count >= categoryLimit)
- {
- break;
- }
+ if (categories.Count >= categoryLimit)
+ {
+ break;
}
}
-
- if (allEmpty)
- {
- break;
- }
}
- return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
- }
-
- private IEnumerable<RecommendationDto> GetWithDirector(
- User? user,
- IEnumerable<string> names,
- int itemLimit,
- DtoOptions dtoOptions,
- RecommendationType type)
- {
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ if (allEmpty)
{
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
+ break;
}
+ }
- foreach (var name in names)
- {
- var items = _libraryManager.GetItemList(
- new InternalItemsQuery(user)
- {
- Person = name,
- // Account for duplicates by IMDb id, since the database doesn't support this yet
- Limit = itemLimit + 2,
- PersonTypes = new[] { PersonType.Director },
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Take(itemLimit)
- .ToList();
-
- if (items.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
+ return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
+ }
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
+ private IEnumerable<RecommendationDto> GetWithDirector(
+ User? user,
+ IEnumerable<string> names,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ RecommendationType type)
+ {
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
}
- private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ foreach (var name in names)
{
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
-
- 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
Limit = itemLimit + 2,
+ PersonTypes = new[] { PersonType.Director },
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
}).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Take(itemLimit)
- .ToList();
+ .Take(itemLimit)
+ .ToList();
- if (items.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
+ if (items.Count > 0)
+ {
+ var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
+ yield return new RecommendationDto
+ {
+ BaselineItemName = name,
+ CategoryId = name.GetMD5(),
+ RecommendationType = type,
+ Items = returnItems
+ };
}
}
+ }
+
+ private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ {
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
- private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ foreach (var name in names)
{
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
+ Person = name,
+ // Account for duplicates by IMDb id, since the database doesn't support this yet
+ Limit = itemLimit + 2,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+ .Take(itemLimit)
+ .ToList();
- foreach (var item in baselineItems)
+ if (items.Count > 0)
{
- var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Limit = itemLimit,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- SimilarTo = item,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- });
+ var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
- if (similar.Count > 0)
+ yield return new RecommendationDto
{
- var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = item.Name,
- CategoryId = item.Id,
- RecommendationType = type,
- Items = returnItems
- };
- }
+ BaselineItemName = name,
+ CategoryId = name.GetMD5(),
+ RecommendationType = type,
+ Items = returnItems
+ };
}
}
+ }
- private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
+ private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ {
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+
+ foreach (var item in baselineItems)
+ {
+ var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- MaxListOrder = 3
+ Limit = itemLimit,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ SimilarTo = item,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
});
- var itemIds = items.Select(i => i.Id).ToList();
+ if (similar.Count > 0)
+ {
+ var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
+ yield return new RecommendationDto
+ {
+ BaselineItemName = item.Name,
+ CategoryId = item.Id,
+ RecommendationType = type,
+ Items = returnItems
+ };
+ }
}
+ }
- private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
+ private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
+ {
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery(
- new[] { PersonType.Director },
- Array.Empty<string>()));
+ MaxListOrder = 3
+ });
- var itemIds = items.Select(i => i.Id).ToList();
+ var itemIds = items.Select(i => i.Id).ToList();
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
- }
+ return people
+ .Where(i => itemIds.Contains(i.ItemId))
+ .Select(i => i.Name)
+ .DistinctNames();
+ }
+
+ private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
+ {
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery(
+ new[] { PersonType.Director },
+ Array.Empty<string>()));
+
+ var itemIds = items.Select(i => i.Id).ToList();
+
+ return people
+ .Where(i => itemIds.Contains(i.ItemId))
+ .Select(i => i.Name)
+ .DistinctNames();
}
}
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index f4fb5f44a..435457af6 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -18,181 +17,187 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The music genres controller.
+/// </summary>
+[Authorize]
+public class MusicGenresController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// The music genres controller.
+ /// Initializes a new instance of the <see cref="MusicGenresController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class MusicGenresController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ public MusicGenresController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MusicGenresController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
- public MusicGenresController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- }
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Gets all music genres from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">The search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
- /// <param name="userId">User id.</param>
- /// <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>
- /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
- [HttpGet]
- [Obsolete("Use GetGenres instead")]
- public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isFavorite,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [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)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
+ /// <summary>
+ /// Gets all music genres from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">The search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
+ /// <param name="userId">User id.</param>
+ /// <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>
+ /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
+ [HttpGet]
+ [Obsolete("Use GetGenres instead")]
+ public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ User? user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- var parentItem = _libraryManager.GetParentItem(parentId, userId);
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
- var query = new InternalItemsQuery(user)
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
+ };
+
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
+ query.AncestorIds = new[] { parentId.Value };
+ }
+ else
{
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { parentId.Value };
- }
- else
- {
- query.ItemIds = new[] { parentId.Value };
- }
+ query.ItemIds = new[] { parentId.Value };
}
+ }
- var result = _libraryManager.GetMusicGenres(query);
+ var result = _libraryManager.GetMusicGenres(query);
- var shouldIncludeItemTypes = includeItemTypes.Length != 0;
- return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
- }
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
+ }
- /// <summary>
- /// Gets a music genre, by name.
- /// </summary>
- /// <param name="genreName">The genre name.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <returns>An <see cref="OkResult"/> containing a <see cref="BaseItemDto"/> with the music genre.</returns>
- [HttpGet("{genreName}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
- {
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ /// <summary>
+ /// Gets a music genre, by name.
+ /// </summary>
+ /// <param name="genreName">The genre name.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <returns>An <see cref="OkResult"/> containing a <see cref="BaseItemDto"/> with the music genre.</returns>
+ [HttpGet("{genreName}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- MusicGenre? item;
+ MusicGenre? item;
- if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
- {
- item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
- }
- else
- {
- item = _libraryManager.GetMusicGenre(genreName);
- }
+ if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
+ }
+ else
+ {
+ item = _libraryManager.GetMusicGenre(genreName);
+ }
- if (userId.HasValue && !userId.Value.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ if (!userId.Value.Equals(default))
+ {
+ var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
- private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
- where T : BaseItem, new()
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
+ }
+
+ private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
+ where T : BaseItem, new()
+ {
+ var result = libraryManager.GetItemList(new InternalItemsQuery
{
- var result = libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '&'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ Name = name.Replace(BaseItem.SlugChar, '&'),
+ IncludeItemTypes = new[] { baseItemKind },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
- result ??= libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '/'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '/'),
+ IncludeItemTypes = new[] { baseItemKind },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
- result ??= libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '?'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '?'),
+ IncludeItemTypes = new[] { baseItemKind },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
- return result;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
deleted file mode 100644
index a28556476..000000000
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Collections.Generic;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Jellyfin.Api.Controllers
-{
- /// <summary>
- /// The notification controller.
- /// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class NotificationsController : BaseJellyfinApiController
- {
- private readonly INotificationManager _notificationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="NotificationsController" /> class.
- /// </summary>
- /// <param name="notificationManager">The notification manager.</param>
- public NotificationsController(INotificationManager notificationManager)
- {
- _notificationManager = notificationManager;
- }
-
- /// <summary>
- /// Gets notification types.
- /// </summary>
- /// <response code="200">All notification types returned.</response>
- /// <returns>An <cref see="OkResult"/> containing a list of all notification types.</returns>
- [HttpGet("Types")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
- {
- return _notificationManager.GetNotificationTypes();
- }
-
- /// <summary>
- /// Gets notification services.
- /// </summary>
- /// <response code="200">All notification services returned.</response>
- /// <returns>An <cref see="OkResult"/> containing a list of all notification services.</returns>
- [HttpGet("Services")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<NameIdPair> GetNotificationServices()
- {
- return _notificationManager.GetNotificationServices();
- }
- }
-}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 10f967dcd..0ba5e995f 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -11,157 +11,156 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Package Controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class PackageController : BaseJellyfinApiController
{
+ private readonly IInstallationManager _installationManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Package Controller.
+ /// Initializes a new instance of the <see cref="PackageController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PackageController : BaseJellyfinApiController
+ /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
{
- private readonly IInstallationManager _installationManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PackageController"/> class.
- /// </summary>
- /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
- {
- _installationManager = installationManager;
- _serverConfigurationManager = serverConfigurationManager;
- }
+ _installationManager = installationManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <summary>
- /// Gets a package by name or assembly GUID.
- /// </summary>
- /// <param name="name">The name of the package.</param>
- /// <param name="assemblyGuid">The GUID of the associated assembly.</param>
- /// <response code="200">Package retrieved.</response>
- /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
- [HttpGet("Packages/{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PackageInfo>> GetPackageInfo(
- [FromRoute, Required] string name,
- [FromQuery] Guid? assemblyGuid)
+ /// <summary>
+ /// Gets a package by name or assembly GUID.
+ /// </summary>
+ /// <param name="name">The name of the package.</param>
+ /// <param name="assemblyGuid">The GUID of the associated assembly.</param>
+ /// <response code="200">Package retrieved.</response>
+ /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
+ [HttpGet("Packages/{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PackageInfo>> GetPackageInfo(
+ [FromRoute, Required] string name,
+ [FromQuery] Guid? assemblyGuid)
+ {
+ var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ var result = _installationManager.FilterPackages(
+ packages,
+ name,
+ assemblyGuid ?? default)
+ .FirstOrDefault();
+
+ if (result is null)
{
- var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
- var result = _installationManager.FilterPackages(
- packages,
- name,
- assemblyGuid ?? default)
- .FirstOrDefault();
-
- if (result is null)
- {
- return NotFound();
- }
-
- return result;
+ return NotFound();
}
- /// <summary>
- /// Gets available packages.
- /// </summary>
- /// <response code="200">Available packages returned.</response>
- /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
- [HttpGet("Packages")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<IEnumerable<PackageInfo>> GetPackages()
- {
- IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ return result;
+ }
- return packages;
- }
+ /// <summary>
+ /// Gets available packages.
+ /// </summary>
+ /// <response code="200">Available packages returned.</response>
+ /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
+ [HttpGet("Packages")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<IEnumerable<PackageInfo>> GetPackages()
+ {
+ IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
- /// <summary>
- /// Installs a package.
- /// </summary>
- /// <param name="name">Package name.</param>
- /// <param name="assemblyGuid">GUID of the associated assembly.</param>
- /// <param name="version">Optional version. Defaults to latest version.</param>
- /// <param name="repositoryUrl">Optional. Specify the repository to install from.</param>
- /// <response code="204">Package found.</response>
- /// <response code="404">Package not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
- [HttpPost("Packages/Installed/{name}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Authorize(Policy = Policies.RequiresElevation)]
- public async Task<ActionResult> InstallPackage(
- [FromRoute, Required] string name,
- [FromQuery] Guid? assemblyGuid,
- [FromQuery] string? version,
- [FromQuery] string? repositoryUrl)
- {
- var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
- if (!string.IsNullOrEmpty(repositoryUrl))
- {
- packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
- .ToList();
- }
-
- var package = _installationManager.GetCompatibleVersions(
- packages,
- name,
- assemblyGuid ?? Guid.Empty,
- specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
- .FirstOrDefault();
-
- if (package is null)
- {
- return NotFound();
- }
-
- await _installationManager.InstallPackage(package).ConfigureAwait(false);
-
- return NoContent();
- }
+ return packages;
+ }
- /// <summary>
- /// Cancels a package installation.
- /// </summary>
- /// <param name="packageId">Installation Id.</param>
- /// <response code="204">Installation cancelled.</response>
- /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
- [HttpDelete("Packages/Installing/{packageId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CancelPackageInstallation(
- [FromRoute, Required] Guid packageId)
+ /// <summary>
+ /// Installs a package.
+ /// </summary>
+ /// <param name="name">Package name.</param>
+ /// <param name="assemblyGuid">GUID of the associated assembly.</param>
+ /// <param name="version">Optional version. Defaults to latest version.</param>
+ /// <param name="repositoryUrl">Optional. Specify the repository to install from.</param>
+ /// <response code="204">Package found.</response>
+ /// <response code="404">Package not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
+ [HttpPost("Packages/Installed/{name}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public async Task<ActionResult> InstallPackage(
+ [FromRoute, Required] string name,
+ [FromQuery] Guid? assemblyGuid,
+ [FromQuery] string? version,
+ [FromQuery] string? repositoryUrl)
+ {
+ var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(repositoryUrl))
{
- _installationManager.CancelInstallation(packageId);
- return NoContent();
+ packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
+ .ToList();
}
- /// <summary>
- /// Gets all package repositories.
- /// </summary>
- /// <response code="200">Package repositories returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
- [HttpGet("Repositories")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
- {
- return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable());
- }
+ var package = _installationManager.GetCompatibleVersions(
+ packages,
+ name,
+ assemblyGuid ?? Guid.Empty,
+ specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
+ .FirstOrDefault();
- /// <summary>
- /// Sets the enabled and existing package repositories.
- /// </summary>
- /// <param name="repositoryInfos">The list of package repositories.</param>
- /// <response code="204">Package repositories saved.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Repositories")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
+ if (package is null)
{
- _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
- _serverConfigurationManager.SaveConfiguration();
- return NoContent();
+ return NotFound();
}
+
+ await _installationManager.InstallPackage(package).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Cancels a package installation.
+ /// </summary>
+ /// <param name="packageId">Installation Id.</param>
+ /// <response code="204">Installation cancelled.</response>
+ /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
+ [HttpDelete("Packages/Installing/{packageId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CancelPackageInstallation(
+ [FromRoute, Required] Guid packageId)
+ {
+ _installationManager.CancelInstallation(packageId);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets all package repositories.
+ /// </summary>
+ /// <response code="200">Package repositories returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
+ [HttpGet("Repositories")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
+ {
+ return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable());
+ }
+
+ /// <summary>
+ /// Sets the enabled and existing package repositories.
+ /// </summary>
+ /// <param name="repositoryInfos">The list of package repositories.</param>
+ /// <response code="204">Package repositories saved.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Repositories")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
+ {
+ _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
+ _serverConfigurationManager.SaveConfiguration();
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 09f7281ec..b4c6f490a 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,8 +1,8 @@
using System;
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;
@@ -15,125 +15,126 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Persons controller.
+/// </summary>
+[Authorize]
+public class PersonsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// Persons controller.
+ /// Initializes a new instance of the <see cref="PersonsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PersonsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public PersonsController(
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IUserManager userManager)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="PersonsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public PersonsController(
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IUserManager userManager)
- {
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _userManager = userManager;
- }
+ /// <summary>
+ /// Gets all persons.
+ /// </summary>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">The search term.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</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>
+ /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <response code="200">Persons returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetPersons(
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery] Guid? appearsInItemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool? enableImages = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <summary>
- /// Gets all persons.
- /// </summary>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">The search term.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</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>
- /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
- /// <param name="userId">User id.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <response code="200">Persons returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetPersons(
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery] bool? isFavorite,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery] Guid? appearsInItemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool? enableImages = true)
+ var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
+ var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery(
+ personTypes,
+ excludePersonTypes)
{
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ NameContains = searchTerm,
+ User = user,
+ IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
+ AppearsInItemId = appearsInItemId ?? Guid.Empty,
+ Limit = limit ?? 0
+ });
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ return new QueryResult<BaseItemDto>(
+ peopleItems
+ .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
+ .ToArray());
+ }
- var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
- var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery(
- personTypes,
- excludePersonTypes)
- {
- NameContains = searchTerm,
- User = user,
- IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
- AppearsInItemId = appearsInItemId ?? Guid.Empty,
- Limit = limit ?? 0
- });
+ /// <summary>
+ /// Get person by name.
+ /// </summary>
+ /// <param name="name">Person name.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Person returned.</response>
+ /// <response code="404">Person not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the person on success,
+ /// or a <see cref="NotFoundResult"/> if person not found.</returns>
+ [HttpGet("{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
- return new QueryResult<BaseItemDto>(
- peopleItems
- .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
- .ToArray());
+ var item = _libraryManager.GetPerson(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get person by name.
- /// </summary>
- /// <param name="name">Person name.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Person returned.</response>
- /// <response code="404">Person not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the person on success,
- /// or a <see cref="NotFoundResult"/> if person not found.</returns>
- [HttpGet("{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
+ if (!userId.Value.Equals(default))
{
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
-
- var item = _libraryManager.GetPerson(name);
- if (item is null)
- {
- return NotFound();
- }
-
- if (userId.HasValue && !userId.Value.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ var user = _userManager.GetUserById(userId.Value);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index e0c565da1..c6dbea5e2 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -4,8 +4,8 @@ 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;
@@ -20,202 +20,204 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Playlists controller.
+/// </summary>
+[Authorize]
+public class PlaylistsController : BaseJellyfinApiController
{
+ private readonly IPlaylistManager _playlistManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+
/// <summary>
- /// Playlists controller.
+ /// Initializes a new instance of the <see cref="PlaylistsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PlaylistsController : BaseJellyfinApiController
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public PlaylistsController(
+ IDtoService dtoService,
+ IPlaylistManager playlistManager,
+ IUserManager userManager,
+ ILibraryManager libraryManager)
{
- private readonly IPlaylistManager _playlistManager;
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PlaylistsController"/> class.
- /// </summary>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public PlaylistsController(
- IDtoService dtoService,
- IPlaylistManager playlistManager,
- IUserManager userManager,
- ILibraryManager libraryManager)
- {
- _dtoService = dtoService;
- _playlistManager = playlistManager;
- _userManager = userManager;
- _libraryManager = libraryManager;
- }
+ _dtoService = dtoService;
+ _playlistManager = playlistManager;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ }
- /// <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.
- /// The task result contains an <see cref="OkResult"/> indicating success.
- /// </returns>
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
- [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)
+ /// <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.
+ /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// </returns>
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
+ [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)
{
- if (ids.Count == 0)
- {
- ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
- }
-
- var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
- {
- Name = name ?? createPlaylistRequest?.Name,
- ItemIdList = ids,
- UserId = userId ?? createPlaylistRequest?.UserId ?? default,
- MediaType = mediaType ?? createPlaylistRequest?.MediaType
- }).ConfigureAwait(false);
-
- return result;
+ ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
}
- /// <summary>
- /// Adds items to a playlist.
- /// </summary>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="ids">Item id, comma delimited.</param>
- /// <param name="userId">The userId.</param>
- /// <response code="204">Items added to playlist.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("{playlistId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToPlaylist(
- [FromRoute, Required] Guid playlistId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery] Guid? userId)
+ userId ??= createPlaylistRequest?.UserId ?? default;
+ userId = RequestHelpers.GetUserId(User, userId);
+ var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{
- await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
- return NoContent();
- }
+ Name = name ?? createPlaylistRequest?.Name,
+ ItemIdList = ids,
+ UserId = userId.Value,
+ MediaType = mediaType ?? createPlaylistRequest?.MediaType
+ }).ConfigureAwait(false);
- /// <summary>
- /// Moves a playlist item.
- /// </summary>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="itemId">The item id.</param>
- /// <param name="newIndex">The new index.</param>
- /// <response code="204">Item moved to new index.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> MoveItem(
- [FromRoute, Required] string playlistId,
- [FromRoute, Required] string itemId,
- [FromRoute, Required] int newIndex)
- {
- await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
- return NoContent();
- }
+ return result;
+ }
- /// <summary>
- /// Removes items from a playlist.
- /// </summary>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="entryIds">The item ids, comma delimited.</param>
- /// <response code="204">Items removed.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpDelete("{playlistId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromPlaylist(
- [FromRoute, Required] string playlistId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
- {
- await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Adds items to a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="ids">Item id, comma delimited.</param>
+ /// <param name="userId">The userId.</param>
+ /// <response code="204">Items added to playlist.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+ [HttpPost("{playlistId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> AddToPlaylist(
+ [FromRoute, Required] Guid playlistId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Gets the original items of a playlist.
- /// </summary>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="userId">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="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">Original playlist returned.</response>
- /// <response code="404">Playlist not found.</response>
- /// <returns>The original playlist items.</returns>
- [HttpGet("{playlistId}/Items")]
- public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
- [FromRoute, Required] Guid playlistId,
- [FromQuery, Required] Guid userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableImages,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
- {
- var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
- if (playlist is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Moves a playlist item.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="newIndex">The new index.</param>
+ /// <response code="204">Item moved to new index.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+ [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> MoveItem(
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] string itemId,
+ [FromRoute, Required] int newIndex)
+ {
+ await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
+ return NoContent();
+ }
- var user = userId.Equals(default)
- ? null
- : _userManager.GetUserById(userId);
+ /// <summary>
+ /// Removes items from a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="entryIds">The item ids, comma delimited.</param>
+ /// <response code="204">Items removed.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+ [HttpDelete("{playlistId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RemoveFromPlaylist(
+ [FromRoute, Required] string playlistId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
+ {
+ await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
+ return NoContent();
+ }
- var items = playlist.GetManageableItems().ToArray();
+ /// <summary>
+ /// Gets the original items of a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">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="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">Original playlist returned.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>The original playlist items.</returns>
+ [HttpGet("{playlistId}/Items")]
+ public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
+ [FromRoute, Required] Guid playlistId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ {
+ var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
+ if (playlist is null)
+ {
+ return NotFound();
+ }
- var count = items.Length;
+ var user = userId.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId);
- if (startIndex.HasValue)
- {
- items = items.Skip(startIndex.Value).ToArray();
- }
+ var items = playlist.GetManageableItems().ToArray();
- if (limit.HasValue)
- {
- items = items.Take(limit.Value).ToArray();
- }
+ var count = items.Length;
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ if (startIndex.HasValue)
+ {
+ items = items.Skip(startIndex.Value).ToArray();
+ }
- var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
+ if (limit.HasValue)
+ {
+ items = items.Take(limit.Value).ToArray();
+ }
- for (int index = 0; index < dtos.Count; index++)
- {
- dtos[index].PlaylistItemId = items[index].Item1.Id;
- }
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var result = new QueryResult<BaseItemDto>(
- startIndex,
- count,
- dtos);
+ var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
- return result;
+ for (int index = 0; index < dtos.Count; index++)
+ {
+ dtos[index].PlaylistItemId = items[index].Item1.Id;
}
+
+ var result = new QueryResult<BaseItemDto>(
+ startIndex,
+ count,
+ dtos);
+
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 58f9b7d35..8ad553bcb 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -2,11 +2,11 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
@@ -16,350 +16,385 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Playstate controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class PlaystateController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepository;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly ILogger<PlaystateController> _logger;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+
/// <summary>
- /// Playstate controller.
+ /// Initializes a new instance of the <see cref="PlaystateController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PlaystateController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
+ public PlaystateController(
+ IUserManager userManager,
+ IUserDataManager userDataRepository,
+ ILibraryManager libraryManager,
+ ISessionManager sessionManager,
+ ILoggerFactory loggerFactory,
+ TranscodingJobHelper transcodingJobHelper)
{
- private readonly IUserManager _userManager;
- private readonly IUserDataManager _userDataRepository;
- private readonly ILibraryManager _libraryManager;
- private readonly ISessionManager _sessionManager;
- private readonly ILogger<PlaystateController> _logger;
- private readonly TranscodingJobHelper _transcodingJobHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PlaystateController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
- /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
- public PlaystateController(
- IUserManager userManager,
- IUserDataManager userDataRepository,
- ILibraryManager libraryManager,
- ISessionManager sessionManager,
- ILoggerFactory loggerFactory,
- TranscodingJobHelper transcodingJobHelper)
- {
- _userManager = userManager;
- _userDataRepository = userDataRepository;
- _libraryManager = libraryManager;
- _sessionManager = sessionManager;
- _logger = loggerFactory.CreateLogger<PlaystateController>();
+ _userManager = userManager;
+ _userDataRepository = userDataRepository;
+ _libraryManager = libraryManager;
+ _sessionManager = sessionManager;
+ _logger = loggerFactory.CreateLogger<PlaystateController>();
- _transcodingJobHelper = transcodingJobHelper;
- }
+ _transcodingJobHelper = transcodingJobHelper;
+ }
- /// <summary>
- /// Marks an item as played for user.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <param name="datePlayed">Optional. The date the item was played.</param>
- /// <response code="200">Item marked as played.</response>
- /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpPost("Users/{userId}/PlayedItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] Guid itemId,
- [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
+ /// <summary>
+ /// Marks an item as played for user.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="datePlayed">Optional. The date the item was played.</param>
+ /// <response code="200">Item marked as played.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
+ [HttpPost("Users/{userId}/PlayedItems/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
{
- var user = _userManager.GetUserById(userId);
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var dto = UpdatePlayedStatus(user, itemId, true, datePlayed);
- foreach (var additionalUserInfo in session.AdditionalUsers)
- {
- var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
- UpdatePlayedStatus(additionalUser, itemId, true, datePlayed);
- }
+ return NotFound();
+ }
+
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- return dto;
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Marks an item as unplayed for user.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Item marked as unplayed.</response>
- /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ var dto = UpdatePlayedStatus(user, item, true, datePlayed);
+ foreach (var additionalUserInfo in session.AdditionalUsers)
{
- var user = _userManager.GetUserById(userId);
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var dto = UpdatePlayedStatus(user, itemId, false, null);
- foreach (var additionalUserInfo in session.AdditionalUsers)
+ var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+ if (additionalUser is null)
{
- var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
- UpdatePlayedStatus(additionalUser, itemId, false, null);
+ return NotFound();
}
- return dto;
+ UpdatePlayedStatus(additionalUser, item, true, datePlayed);
}
- /// <summary>
- /// Reports playback has started within a session.
- /// </summary>
- /// <param name="playbackStartInfo">The playback start info.</param>
- /// <response code="204">Playback start recorded.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Playing")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
- {
- playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
- playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
- return NoContent();
- }
+ return dto;
+ }
- /// <summary>
- /// Reports playback progress within a session.
- /// </summary>
- /// <param name="playbackProgressInfo">The playback progress info.</param>
- /// <response code="204">Playback progress recorded.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Playing/Progress")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
+ /// <summary>
+ /// Marks an item as unplayed for user.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Item marked as unplayed.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
+ [HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
{
- playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
- playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
- return NoContent();
+ return NotFound();
}
- /// <summary>
- /// Pings a playback session.
- /// </summary>
- /// <param name="playSessionId">Playback session id.</param>
- /// <response code="204">Playback session pinged.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Playing/Ping")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item is null)
{
- _transcodingJobHelper.PingTranscodingJob(playSessionId, null);
- return NoContent();
+ return NotFound();
}
- /// <summary>
- /// Reports playback has stopped within a session.
- /// </summary>
- /// <param name="playbackStopInfo">The playback stop info.</param>
- /// <response code="204">Playback stop recorded.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Playing/Stopped")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo)
+ var dto = UpdatePlayedStatus(user, item, false, null);
+ foreach (var additionalUserInfo in session.AdditionalUsers)
{
- _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
- if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
+ var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+ if (additionalUser is null)
{
- await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ return NotFound();
}
- playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
- return NoContent();
+ UpdatePlayedStatus(additionalUser, item, false, null);
}
- /// <summary>
- /// Reports that a user has begun playing an item.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <param name="mediaSourceId">The id of the MediaSource.</param>
- /// <param name="audioStreamIndex">The audio stream index.</param>
- /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
- /// <param name="playMethod">The play method.</param>
- /// <param name="liveStreamId">The live stream id.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <param name="canSeek">Indicates if the client can seek.</param>
- /// <response code="204">Play start recorded.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Users/{userId}/PlayingItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
- public async Task<ActionResult> OnPlaybackStart(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? mediaSourceId,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] PlayMethod? playMethod,
- [FromQuery] string? liveStreamId,
- [FromQuery] string? playSessionId,
- [FromQuery] bool canSeek = false)
+ return dto;
+ }
+
+ /// <summary>
+ /// Reports playback has started within a session.
+ /// </summary>
+ /// <param name="playbackStartInfo">The playback start info.</param>
+ /// <response code="204">Playback start recorded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Playing")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
+ {
+ playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
+ playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Reports playback progress within a session.
+ /// </summary>
+ /// <param name="playbackProgressInfo">The playback progress info.</param>
+ /// <response code="204">Playback progress recorded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Playing/Progress")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
+ {
+ playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
+ playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Pings a playback session.
+ /// </summary>
+ /// <param name="playSessionId">Playback session id.</param>
+ /// <response code="204">Playback session pinged.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Playing/Ping")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
+ {
+ _transcodingJobHelper.PingTranscodingJob(playSessionId, null);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Reports playback has stopped within a session.
+ /// </summary>
+ /// <param name="playbackStopInfo">The playback stop info.</param>
+ /// <response code="204">Playback stop recorded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Playing/Stopped")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo)
+ {
+ _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
- var playbackStartInfo = new PlaybackStartInfo
- {
- CanSeek = canSeek,
- ItemId = itemId,
- MediaSourceId = mediaSourceId,
- AudioStreamIndex = audioStreamIndex,
- SubtitleStreamIndex = subtitleStreamIndex,
- PlayMethod = playMethod ?? PlayMethod.Transcode,
- PlaySessionId = playSessionId,
- LiveStreamId = liveStreamId
- };
-
- playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
- playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
- return NoContent();
+ await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
- /// <summary>
- /// Reports a user's playback progress.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <param name="mediaSourceId">The id of the MediaSource.</param>
- /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="audioStreamIndex">The audio stream index.</param>
- /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
- /// <param name="volumeLevel">Scale of 0-100.</param>
- /// <param name="playMethod">The play method.</param>
- /// <param name="liveStreamId">The live stream id.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <param name="repeatMode">The repeat mode.</param>
- /// <param name="isPaused">Indicates if the player is paused.</param>
- /// <param name="isMuted">Indicates if the player is muted.</param>
- /// <response code="204">Play progress recorded.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
- public async Task<ActionResult> OnPlaybackProgress(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? mediaSourceId,
- [FromQuery] long? positionTicks,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] int? volumeLevel,
- [FromQuery] PlayMethod? playMethod,
- [FromQuery] string? liveStreamId,
- [FromQuery] string? playSessionId,
- [FromQuery] RepeatMode? repeatMode,
- [FromQuery] bool isPaused = false,
- [FromQuery] bool isMuted = false)
+ playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Reports that a user has begun playing an item.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="mediaSourceId">The id of the MediaSource.</param>
+ /// <param name="audioStreamIndex">The audio stream index.</param>
+ /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+ /// <param name="playMethod">The play method.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="canSeek">Indicates if the client can seek.</param>
+ /// <response code="204">Play start recorded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Users/{userId}/PlayingItems/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
+ public async Task<ActionResult> OnPlaybackStart(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] PlayMethod? playMethod,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] bool canSeek = false)
+ {
+ var playbackStartInfo = new PlaybackStartInfo
{
- var playbackProgressInfo = new PlaybackProgressInfo
- {
- ItemId = itemId,
- PositionTicks = positionTicks,
- IsMuted = isMuted,
- IsPaused = isPaused,
- MediaSourceId = mediaSourceId,
- AudioStreamIndex = audioStreamIndex,
- SubtitleStreamIndex = subtitleStreamIndex,
- VolumeLevel = volumeLevel,
- PlayMethod = playMethod ?? PlayMethod.Transcode,
- PlaySessionId = playSessionId,
- LiveStreamId = liveStreamId,
- RepeatMode = repeatMode ?? RepeatMode.RepeatNone
- };
-
- playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
- playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
- return NoContent();
- }
+ CanSeek = canSeek,
+ ItemId = itemId,
+ MediaSourceId = mediaSourceId,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ PlayMethod = playMethod ?? PlayMethod.Transcode,
+ PlaySessionId = playSessionId,
+ LiveStreamId = liveStreamId
+ };
+
+ playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
+ playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Reports that a user has stopped playing an item.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <param name="mediaSourceId">The id of the MediaSource.</param>
- /// <param name="nextMediaType">The next media type that will play.</param>
- /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
- /// <param name="liveStreamId">The live stream id.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <response code="204">Playback stop recorded.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Users/{userId}/PlayingItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
- public async Task<ActionResult> OnPlaybackStopped(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? nextMediaType,
- [FromQuery] long? positionTicks,
- [FromQuery] string? liveStreamId,
- [FromQuery] string? playSessionId)
+ /// <summary>
+ /// Reports a user's playback progress.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="mediaSourceId">The id of the MediaSource.</param>
+ /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="audioStreamIndex">The audio stream index.</param>
+ /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+ /// <param name="volumeLevel">Scale of 0-100.</param>
+ /// <param name="playMethod">The play method.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="repeatMode">The repeat mode.</param>
+ /// <param name="isPaused">Indicates if the player is paused.</param>
+ /// <param name="isMuted">Indicates if the player is muted.</param>
+ /// <response code="204">Play progress recorded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
+ public async Task<ActionResult> OnPlaybackProgress(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] long? positionTicks,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? volumeLevel,
+ [FromQuery] PlayMethod? playMethod,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] RepeatMode? repeatMode,
+ [FromQuery] bool isPaused = false,
+ [FromQuery] bool isMuted = false)
+ {
+ var playbackProgressInfo = new PlaybackProgressInfo
{
- var playbackStopInfo = new PlaybackStopInfo
- {
- ItemId = itemId,
- PositionTicks = positionTicks,
- MediaSourceId = mediaSourceId,
- PlaySessionId = playSessionId,
- LiveStreamId = liveStreamId,
- NextMediaType = nextMediaType
- };
-
- _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
- if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
- {
- await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
- }
+ ItemId = itemId,
+ PositionTicks = positionTicks,
+ IsMuted = isMuted,
+ IsPaused = isPaused,
+ MediaSourceId = mediaSourceId,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ VolumeLevel = volumeLevel,
+ PlayMethod = playMethod ?? PlayMethod.Transcode,
+ PlaySessionId = playSessionId,
+ LiveStreamId = liveStreamId,
+ RepeatMode = repeatMode ?? RepeatMode.RepeatNone
+ };
- playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
- return NoContent();
- }
+ playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
+ playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Reports that a user has stopped playing an item.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="mediaSourceId">The id of the MediaSource.</param>
+ /// <param name="nextMediaType">The next media type that will play.</param>
+ /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <response code="204">Playback stop recorded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Users/{userId}/PlayingItems/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
+ public async Task<ActionResult> OnPlaybackStopped(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? nextMediaType,
+ [FromQuery] long? positionTicks,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] string? playSessionId)
+ {
+ var playbackStopInfo = new PlaybackStopInfo
+ {
+ ItemId = itemId,
+ PositionTicks = positionTicks,
+ MediaSourceId = mediaSourceId,
+ PlaySessionId = playSessionId,
+ LiveStreamId = liveStreamId,
+ NextMediaType = nextMediaType
+ };
- /// <summary>
- /// Updates the played status.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <param name="itemId">The item id.</param>
- /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
- /// <param name="datePlayed">The date played.</param>
- /// <returns>Task.</returns>
- private UserItemDataDto UpdatePlayedStatus(User user, Guid itemId, bool wasPlayed, DateTime? datePlayed)
+ _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
- var item = _libraryManager.GetItemById(itemId);
+ await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ }
- if (wasPlayed)
- {
- item.MarkPlayed(user, datePlayed, true);
- }
- else
- {
- item.MarkUnplayed(user);
- }
+ playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
+ return NoContent();
+ }
- return _userDataRepository.GetUserDataDto(item, user);
+ /// <summary>
+ /// Updates the played status.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
+ /// <param name="datePlayed">The date played.</param>
+ /// <returns>Task.</returns>
+ private UserItemDataDto UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
+ {
+ if (wasPlayed)
+ {
+ item.MarkPlayed(user, datePlayed, true);
+ }
+ else
+ {
+ item.MarkUnplayed(user);
}
- private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
+
+ private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
+ {
+ if (method == PlayMethod.Transcode)
{
- if (method == PlayMethod.Transcode)
+ var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
+ if (job is null)
{
- var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
- if (job is null)
- {
- return PlayMethod.DirectPlay;
- }
+ return PlayMethod.DirectPlay;
}
-
- return method;
}
+
+ return method;
}
}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index b8a09990a..4726cf066 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -17,250 +16,249 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Plugins controller.
+/// </summary>
+[Authorize]
+public class PluginsController : BaseJellyfinApiController
{
+ private readonly IInstallationManager _installationManager;
+ private readonly IPluginManager _pluginManager;
+ private readonly JsonSerializerOptions _serializerOptions;
+
/// <summary>
- /// Plugins controller.
+ /// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PluginsController : BaseJellyfinApiController
+ /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+ /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
+ public PluginsController(
+ IInstallationManager installationManager,
+ IPluginManager pluginManager)
{
- private readonly IInstallationManager _installationManager;
- private readonly IPluginManager _pluginManager;
- private readonly JsonSerializerOptions _serializerOptions;
+ _installationManager = installationManager;
+ _pluginManager = pluginManager;
+ _serializerOptions = JsonDefaults.Options;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="PluginsController"/> class.
- /// </summary>
- /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
- /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
- public PluginsController(
- IInstallationManager installationManager,
- IPluginManager pluginManager)
+ /// <summary>
+ /// Gets a list of currently installed plugins.
+ /// </summary>
+ /// <response code="200">Installed plugins returned.</response>
+ /// <returns>List of currently installed plugins.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
+ {
+ return Ok(_pluginManager.Plugins
+ .OrderBy(p => p.Name)
+ .Select(p => p.GetPluginInfo()));
+ }
+
+ /// <summary>
+ /// 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 is null)
{
- _installationManager = installationManager;
- _pluginManager = pluginManager;
- _serializerOptions = JsonDefaults.Options;
+ return NotFound();
}
- /// <summary>
- /// Gets a list of currently installed plugins.
- /// </summary>
- /// <response code="200">Installed plugins returned.</response>
- /// <returns>List of currently installed plugins.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
+ _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 is null)
{
- return Ok(_pluginManager.Plugins
- .OrderBy(p => p.Name)
- .Select(p => p.GetPluginInfo()));
+ return NotFound();
}
- /// <summary>
- /// 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 is null)
- {
- return NotFound();
- }
+ _pluginManager.DisablePlugin(plugin);
+ return NoContent();
+ }
- _pluginManager.EnablePlugin(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 plugin could not be found.</returns>
+ [HttpDelete("{pluginId}/{version}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin is null)
+ {
+ return NotFound();
}
- /// <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 is null)
- {
- return NotFound();
- }
+ _installationManager.UninstallPlugin(plugin);
+ return NoContent();
+ }
- _pluginManager.DisablePlugin(plugin);
- return NoContent();
- }
+ /// <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)).ToList();
- /// <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 plugin could not be found.</returns>
- [HttpDelete("{pluginId}/{version}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
- {
- var plugin = _pluginManager.GetPlugin(pluginId, version);
- if (plugin is null)
- {
- return NotFound();
- }
+ // Select the un-instanced one first.
+ var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
+ if (plugin is not null)
+ {
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
- /// <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)).ToList();
-
- // Select the un-instanced one first.
- var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
-
- if (plugin is not null)
- {
- _installationManager.UninstallPlugin(plugin);
- return NoContent();
- }
+ return NotFound();
+ }
- return NotFound();
+ /// <summary>
+ /// Gets plugin configuration.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <response code="200">Plugin configuration returned.</response>
+ /// <response code="404">Plugin not found or plugin configuration not found.</response>
+ /// <returns>Plugin configuration.</returns>
+ [HttpGet("{pluginId}/Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId);
+ if (plugin?.Instance is IHasPluginConfiguration configPlugin)
+ {
+ return configPlugin.Configuration;
}
- /// <summary>
- /// Gets plugin configuration.
- /// </summary>
- /// <param name="pluginId">Plugin id.</param>
- /// <response code="200">Plugin configuration returned.</response>
- /// <response code="404">Plugin not found or plugin configuration not found.</response>
- /// <returns>Plugin configuration.</returns>
- [HttpGet("{pluginId}/Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
- {
- var plugin = _pluginManager.GetPlugin(pluginId);
- if (plugin?.Instance is IHasPluginConfiguration configPlugin)
- {
- return configPlugin.Configuration;
- }
+ return NotFound();
+ }
+ /// <summary>
+ /// Updates plugin configuration.
+ /// </summary>
+ /// <remarks>
+ /// Accepts plugin configuration as JSON body.
+ /// </remarks>
+ /// <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>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)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId);
+ if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
+ {
return NotFound();
}
- /// <summary>
- /// Updates plugin configuration.
- /// </summary>
- /// <remarks>
- /// Accepts plugin configuration as JSON body.
- /// </remarks>
- /// <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>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)
- {
- var plugin = _pluginManager.GetPlugin(pluginId);
- if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
- {
- return NotFound();
- }
+ var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
+ .ConfigureAwait(false);
- var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
- .ConfigureAwait(false);
+ if (configuration is not null)
+ {
+ configPlugin.UpdateConfiguration(configuration);
+ }
- if (configuration is not null)
- {
- configPlugin.UpdateConfiguration(configuration);
- }
+ return NoContent();
+ }
- return NoContent();
+ /// <summary>
+ /// Gets a plugin's image.
+ /// </summary>
+ /// <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)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ [AllowAnonymous]
+ public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets a plugin's image.
- /// </summary>
- /// <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)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- [AllowAnonymous]
- public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
+ if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath))
{
- var plugin = _pluginManager.GetPlugin(pluginId, version);
- if (plugin is null)
- {
- return NotFound();
- }
-
- var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
- if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath))
- {
- return NotFound();
- }
-
- imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
- return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
+ return NotFound();
}
- /// <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);
+ imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
+ return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
+ }
- if (plugin is not null)
- {
- return plugin.Manifest;
- }
+ /// <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);
- return NotFound();
+ if (plugin is not null)
+ {
+ return plugin.Manifest;
}
+
+ return NotFound();
}
}
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 6dbcdae22..d7e54b5b6 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -13,126 +13,119 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Quick connect controller.
+/// </summary>
+public class QuickConnectController : BaseJellyfinApiController
{
+ private readonly IQuickConnect _quickConnect;
+ private readonly IAuthorizationContext _authContext;
+
/// <summary>
- /// Quick connect controller.
+ /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
/// </summary>
- public class QuickConnectController : BaseJellyfinApiController
+ /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
+ /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
{
- private readonly IQuickConnect _quickConnect;
- private readonly IAuthorizationContext _authContext;
+ _quickConnect = quickConnect;
+ _authContext = authContext;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
- /// </summary>
- /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
- /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
- public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
- {
- _quickConnect = quickConnect;
- _authContext = authContext;
- }
+ /// <summary>
+ /// Gets the current quick connect state.
+ /// </summary>
+ /// <response code="200">Quick connect state returned.</response>
+ /// <returns>Whether Quick Connect is enabled on the server or not.</returns>
+ [HttpGet("Enabled")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<bool> GetQuickConnectEnabled()
+ {
+ return _quickConnect.IsEnabled;
+ }
- /// <summary>
- /// Gets the current quick connect state.
- /// </summary>
- /// <response code="200">Quick connect state returned.</response>
- /// <returns>Whether Quick Connect is enabled on the server or not.</returns>
- [HttpGet("Enabled")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<bool> GetQuickConnectEnabled()
+ /// <summary>
+ /// Initiate a new quick connect request.
+ /// </summary>
+ /// <response code="200">Quick connect request successfully created.</response>
+ /// <response code="401">Quick connect is not active on this server.</response>
+ /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
+ [HttpPost("Initiate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
+ {
+ try
{
- return _quickConnect.IsEnabled;
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ return _quickConnect.TryConnect(auth);
}
-
- /// <summary>
- /// Initiate a new quick connect request.
- /// </summary>
- /// <response code="200">Quick connect request successfully created.</response>
- /// <response code="401">Quick connect is not active on this server.</response>
- /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
- [HttpPost("Initiate")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
+ catch (AuthenticationException)
{
- try
- {
- var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
- return _quickConnect.TryConnect(auth);
- }
- catch (AuthenticationException)
- {
- return Unauthorized("Quick connect is disabled");
- }
+ return Unauthorized("Quick connect is disabled");
}
+ }
- /// <summary>
- /// Old version of <see cref="InitiateQuickConnect" /> using a GET method.
- /// Still available to avoid breaking compatibility.
- /// </summary>
- /// <returns>The result of <see cref="InitiateQuickConnect" />.</returns>
- [Obsolete("Use POST request instead")]
- [HttpGet("Initiate")]
- [ApiExplorerSettings(IgnoreApi = true)]
- public Task<ActionResult<QuickConnectResult>> InitiateQuickConnectLegacy() => InitiateQuickConnect();
+ /// <summary>
+ /// Old version of <see cref="InitiateQuickConnect" /> using a GET method.
+ /// Still available to avoid breaking compatibility.
+ /// </summary>
+ /// <returns>The result of <see cref="InitiateQuickConnect" />.</returns>
+ [Obsolete("Use POST request instead")]
+ [HttpGet("Initiate")]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public Task<ActionResult<QuickConnectResult>> InitiateQuickConnectLegacy() => InitiateQuickConnect();
- /// <summary>
- /// Attempts to retrieve authentication information.
- /// </summary>
- /// <param name="secret">Secret previously returned from the Initiate endpoint.</param>
- /// <response code="200">Quick connect result returned.</response>
- /// <response code="404">Unknown quick connect secret.</response>
- /// <returns>An updated <see cref="QuickConnectResult"/>.</returns>
- [HttpGet("Connect")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<QuickConnectResult> GetQuickConnectState([FromQuery, Required] string secret)
+ /// <summary>
+ /// Attempts to retrieve authentication information.
+ /// </summary>
+ /// <param name="secret">Secret previously returned from the Initiate endpoint.</param>
+ /// <response code="200">Quick connect result returned.</response>
+ /// <response code="404">Unknown quick connect secret.</response>
+ /// <returns>An updated <see cref="QuickConnectResult"/>.</returns>
+ [HttpGet("Connect")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<QuickConnectResult> GetQuickConnectState([FromQuery, Required] string secret)
+ {
+ try
{
- try
- {
- return _quickConnect.CheckRequestStatus(secret);
- }
- catch (ResourceNotFoundException)
- {
- return NotFound("Unknown secret");
- }
- catch (AuthenticationException)
- {
- return Unauthorized("Quick connect is disabled");
- }
+ return _quickConnect.CheckRequestStatus(secret);
}
-
- /// <summary>
- /// Authorizes a pending quick connect request.
- /// </summary>
- /// <param name="code">Quick connect code to authorize.</param>
- /// <param name="userId">The user the authorize. Access to the requested user is required.</param>
- /// <response code="200">Quick connect result authorized successfully.</response>
- /// <response code="403">Unknown user id.</response>
- /// <returns>Boolean indicating if the authorization was successful.</returns>
- [HttpPost("Authorize")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null)
+ catch (ResourceNotFoundException)
+ {
+ return NotFound("Unknown secret");
+ }
+ catch (AuthenticationException)
{
- var currentUserId = User.GetUserId();
- var actualUserId = userId ?? currentUserId;
+ return Unauthorized("Quick connect is disabled");
+ }
+ }
- if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator)))
- {
- return Forbid("Unknown user id");
- }
+ /// <summary>
+ /// Authorizes a pending quick connect request.
+ /// </summary>
+ /// <param name="code">Quick connect code to authorize.</param>
+ /// <param name="userId">The user the authorize. Access to the requested user is required.</param>
+ /// <response code="200">Quick connect result authorized successfully.</response>
+ /// <response code="403">Unknown user id.</response>
+ /// <returns>Boolean indicating if the authorization was successful.</returns>
+ [HttpPost("Authorize")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
- try
- {
- return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
- }
- catch (AuthenticationException)
- {
- return Unauthorized("Quick connect is disabled");
- }
+ try
+ {
+ return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false);
+ }
+ catch (AuthenticationException)
+ {
+ return Unauthorized("Quick connect is disabled");
}
}
}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index da9e8cf90..5c77db240 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -15,165 +15,164 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Remote Images Controller.
+/// </summary>
+[Route("")]
+public class RemoteImageController : BaseJellyfinApiController
{
+ private readonly IProviderManager _providerManager;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RemoteImageController"/> class.
+ /// </summary>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public RemoteImageController(
+ IProviderManager providerManager,
+ IServerApplicationPaths applicationPaths,
+ ILibraryManager libraryManager)
+ {
+ _providerManager = providerManager;
+ _applicationPaths = applicationPaths;
+ _libraryManager = libraryManager;
+ }
+
/// <summary>
- /// Remote Images Controller.
+ /// Gets available remote images for an item.
/// </summary>
- [Route("")]
- public class RemoteImageController : BaseJellyfinApiController
+ /// <param name="itemId">Item Id.</param>
+ /// <param name="type">The image type.</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="providerName">Optional. The image provider to use.</param>
+ /// <param name="includeAllLanguages">Optional. Include all languages.</param>
+ /// <response code="200">Remote Images returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>Remote Image Result.</returns>
+ [HttpGet("Items/{itemId}/RemoteImages")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] ImageType? type,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? providerName,
+ [FromQuery] bool includeAllLanguages = false)
{
- private readonly IProviderManager _providerManager;
- private readonly IServerApplicationPaths _applicationPaths;
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RemoteImageController"/> class.
- /// </summary>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public RemoteImageController(
- IProviderManager providerManager,
- IServerApplicationPaths applicationPaths,
- ILibraryManager libraryManager)
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- _providerManager = providerManager;
- _applicationPaths = applicationPaths;
- _libraryManager = libraryManager;
+ return NotFound();
}
- /// <summary>
- /// Gets available remote images for an item.
- /// </summary>
- /// <param name="itemId">Item Id.</param>
- /// <param name="type">The image type.</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="providerName">Optional. The image provider to use.</param>
- /// <param name="includeAllLanguages">Optional. Include all languages.</param>
- /// <response code="200">Remote Images returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>Remote Image Result.</returns>
- [HttpGet("Items/{itemId}/RemoteImages")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
- [FromRoute, Required] Guid itemId,
- [FromQuery] ImageType? type,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? providerName,
- [FromQuery] bool includeAllLanguages = false)
+ var images = await _providerManager.GetAvailableRemoteImages(
+ item,
+ new RemoteImageQuery(providerName ?? string.Empty)
+ {
+ IncludeAllLanguages = includeAllLanguages,
+ IncludeDisabledProviders = true,
+ ImageType = type
+ },
+ CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var imageArray = images.ToArray();
+ var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
+ if (type.HasValue)
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
-
- var images = await _providerManager.GetAvailableRemoteImages(
- item,
- new RemoteImageQuery(providerName ?? string.Empty)
- {
- IncludeAllLanguages = includeAllLanguages,
- IncludeDisabledProviders = true,
- ImageType = type
- },
- CancellationToken.None)
- .ConfigureAwait(false);
-
- var imageArray = images.ToArray();
- var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
- if (type.HasValue)
- {
- allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
- }
-
- var result = new RemoteImageResult
- {
- TotalRecordCount = imageArray.Length,
- Providers = allProviders.Select(o => o.Name)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray()
- };
-
- if (startIndex.HasValue)
- {
- imageArray = imageArray.Skip(startIndex.Value).ToArray();
- }
-
- if (limit.HasValue)
- {
- imageArray = imageArray.Take(limit.Value).ToArray();
- }
-
- result.Images = imageArray;
- return result;
+ allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
}
- /// <summary>
- /// Gets available remote image providers for an item.
- /// </summary>
- /// <param name="itemId">Item Id.</param>
- /// <response code="200">Returned remote image providers.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>List of remote image providers.</returns>
- [HttpGet("Items/{itemId}/RemoteImages/Providers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
+ var result = new RemoteImageResult
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ TotalRecordCount = imageArray.Length,
+ Providers = allProviders.Select(o => o.Name)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray()
+ };
- return Ok(_providerManager.GetRemoteImageProviderInfo(item));
+ if (startIndex.HasValue)
+ {
+ imageArray = imageArray.Skip(startIndex.Value).ToArray();
}
- /// <summary>
- /// Downloads a remote image for an item.
- /// </summary>
- /// <param name="itemId">Item Id.</param>
- /// <param name="type">The image type.</param>
- /// <param name="imageUrl">The image url.</param>
- /// <response code="204">Remote image downloaded.</response>
- /// <response code="404">Remote image not found.</response>
- /// <returns>Download status.</returns>
- [HttpPost("Items/{itemId}/RemoteImages/Download")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DownloadRemoteImage(
- [FromRoute, Required] Guid itemId,
- [FromQuery, Required] ImageType type,
- [FromQuery] string? imageUrl)
+ if (limit.HasValue)
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ imageArray = imageArray.Take(limit.Value).ToArray();
+ }
- await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
- .ConfigureAwait(false);
+ result.Images = imageArray;
+ return result;
+ }
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
+ /// <summary>
+ /// Gets available remote image providers for an item.
+ /// </summary>
+ /// <param name="itemId">Item Id.</param>
+ /// <response code="200">Returned remote image providers.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>List of remote image providers.</returns>
+ [HttpGet("Items/{itemId}/RemoteImages/Providers")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets the full cache path.
- /// </summary>
- /// <param name="filename">The filename.</param>
- /// <returns>System.String.</returns>
- private string GetFullCachePath(string filename)
+ return Ok(_providerManager.GetRemoteImageProviderInfo(item));
+ }
+
+ /// <summary>
+ /// Downloads a remote image for an item.
+ /// </summary>
+ /// <param name="itemId">Item Id.</param>
+ /// <param name="type">The image type.</param>
+ /// <param name="imageUrl">The image url.</param>
+ /// <response code="204">Remote image downloaded.</response>
+ /// <response code="404">Remote image not found.</response>
+ /// <returns>Download status.</returns>
+ [HttpPost("Items/{itemId}/RemoteImages/Download")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DownloadRemoteImage(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, Required] ImageType type,
+ [FromQuery] string? imageUrl)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
+ return NotFound();
}
+
+ await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets the full cache path.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns>System.String.</returns>
+ private string GetFullCachePath(string filename)
+ {
+ return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
}
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index 832e14505..c8fa11ac6 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -8,154 +8,153 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Scheduled Tasks Controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ScheduledTasksController : BaseJellyfinApiController
{
+ private readonly ITaskManager _taskManager;
+
/// <summary>
- /// Scheduled Tasks Controller.
+ /// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
/// </summary>
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ScheduledTasksController : BaseJellyfinApiController
+ /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
+ public ScheduledTasksController(ITaskManager taskManager)
{
- private readonly ITaskManager _taskManager;
+ _taskManager = taskManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
- /// </summary>
- /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
- public ScheduledTasksController(ITaskManager taskManager)
- {
- _taskManager = taskManager;
- }
+ /// <summary>
+ /// Get tasks.
+ /// </summary>
+ /// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
+ /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
+ /// <response code="200">Scheduled tasks retrieved.</response>
+ /// <returns>The list of scheduled tasks.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<TaskInfo> GetTasks(
+ [FromQuery] bool? isHidden,
+ [FromQuery] bool? isEnabled)
+ {
+ IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
- /// <summary>
- /// Get tasks.
- /// </summary>
- /// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
- /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
- /// <response code="200">Scheduled tasks retrieved.</response>
- /// <returns>The list of scheduled tasks.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<TaskInfo> GetTasks(
- [FromQuery] bool? isHidden,
- [FromQuery] bool? isEnabled)
+ foreach (var task in tasks)
{
- IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
-
- foreach (var task in tasks)
+ if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
{
- if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
+ if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
{
- if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
- {
- continue;
- }
-
- if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
- {
- continue;
- }
+ continue;
}
- yield return ScheduledTaskHelpers.GetTaskInfo(task);
+ if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
+ {
+ continue;
+ }
}
- }
- /// <summary>
- /// Get task by id.
- /// </summary>
- /// <param name="taskId">Task Id.</param>
- /// <response code="200">Task retrieved.</response>
- /// <response code="404">Task not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns>
- [HttpGet("{taskId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<TaskInfo> GetTask([FromRoute, Required] string taskId)
- {
- var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
- string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
+ yield return ScheduledTaskHelpers.GetTaskInfo(task);
+ }
+ }
- if (task is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Get task by id.
+ /// </summary>
+ /// <param name="taskId">Task Id.</param>
+ /// <response code="200">Task retrieved.</response>
+ /// <response code="404">Task not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns>
+ [HttpGet("{taskId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<TaskInfo> GetTask([FromRoute, Required] string taskId)
+ {
+ var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
+ string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
- return ScheduledTaskHelpers.GetTaskInfo(task);
+ if (task is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Start specified task.
- /// </summary>
- /// <param name="taskId">Task Id.</param>
- /// <response code="204">Task started.</response>
- /// <response code="404">Task not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
- [HttpPost("Running/{taskId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult StartTask([FromRoute, Required] string taskId)
- {
- var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
- o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+ return ScheduledTaskHelpers.GetTaskInfo(task);
+ }
- if (task is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Start specified task.
+ /// </summary>
+ /// <param name="taskId">Task Id.</param>
+ /// <response code="204">Task started.</response>
+ /// <response code="404">Task not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
+ [HttpPost("Running/{taskId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult StartTask([FromRoute, Required] string taskId)
+ {
+ var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+ o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
- _taskManager.Execute(task, new TaskOptions());
- return NoContent();
+ if (task is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Stop specified task.
- /// </summary>
- /// <param name="taskId">Task Id.</param>
- /// <response code="204">Task stopped.</response>
- /// <response code="404">Task not found.</response>
- /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
- [HttpDelete("Running/{taskId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult StopTask([FromRoute, Required] string taskId)
- {
- var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
- o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+ _taskManager.Execute(task, new TaskOptions());
+ return NoContent();
+ }
- if (task is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Stop specified task.
+ /// </summary>
+ /// <param name="taskId">Task Id.</param>
+ /// <response code="204">Task stopped.</response>
+ /// <response code="404">Task not found.</response>
+ /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
+ [HttpDelete("Running/{taskId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult StopTask([FromRoute, Required] string taskId)
+ {
+ var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+ o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
- _taskManager.Cancel(task);
- return NoContent();
+ if (task is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Update specified task triggers.
- /// </summary>
- /// <param name="taskId">Task Id.</param>
- /// <param name="triggerInfos">Triggers.</param>
- /// <response code="204">Task triggers updated.</response>
- /// <response code="404">Task not found.</response>
- /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
- [HttpPost("{taskId}/Triggers")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateTask(
- [FromRoute, Required] string taskId,
- [FromBody, Required] TaskTriggerInfo[] triggerInfos)
- {
- var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
- o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
- if (task is null)
- {
- return NotFound();
- }
+ _taskManager.Cancel(task);
+ return NoContent();
+ }
- task.Triggers = triggerInfos;
- return NoContent();
+ /// <summary>
+ /// Update specified task triggers.
+ /// </summary>
+ /// <param name="taskId">Task Id.</param>
+ /// <param name="triggerInfos">Triggers.</param>
+ /// <response code="204">Task triggers updated.</response>
+ /// <response code="404">Task not found.</response>
+ /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
+ [HttpPost("{taskId}/Triggers")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult UpdateTask(
+ [FromRoute, Required] string taskId,
+ [FromBody, Required] TaskTriggerInfo[] triggerInfos)
+ {
+ var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+ o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+ if (task is null)
+ {
+ return NotFound();
}
+
+ task.Triggers = triggerInfos;
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 3b7719f37..f638c31c3 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -20,247 +21,247 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Search controller.
+/// </summary>
+[Route("Search/Hints")]
+[Authorize]
+public class SearchController : BaseJellyfinApiController
{
+ private readonly ISearchEngine _searchEngine;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IImageProcessor _imageProcessor;
+
/// <summary>
- /// Search controller.
+ /// Initializes a new instance of the <see cref="SearchController"/> class.
/// </summary>
- [Route("Search/Hints")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class SearchController : BaseJellyfinApiController
+ /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
+ public SearchController(
+ ISearchEngine searchEngine,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IImageProcessor imageProcessor)
{
- private readonly ISearchEngine _searchEngine;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IImageProcessor _imageProcessor;
+ _searchEngine = searchEngine;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _imageProcessor = imageProcessor;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="SearchController"/> class.
- /// </summary>
- /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
- /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
- public SearchController(
- ISearchEngine searchEngine,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IImageProcessor imageProcessor)
+ /// <summary>
+ /// Gets the search hint result.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
+ /// <param name="searchTerm">The search term to filter on.</param>
+ /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param>
+ /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param>
+ /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param>
+ /// <param name="parentId">If specified, only children of the parent are returned.</param>
+ /// <param name="isMovie">Optional filter for movies.</param>
+ /// <param name="isSeries">Optional filter for series.</param>
+ /// <param name="isNews">Optional filter for news.</param>
+ /// <param name="isKids">Optional filter for kids.</param>
+ /// <param name="isSports">Optional filter for sports.</param>
+ /// <param name="includePeople">Optional filter whether to include people.</param>
+ /// <param name="includeMedia">Optional filter whether to include media.</param>
+ /// <param name="includeGenres">Optional filter whether to include genres.</param>
+ /// <param name="includeStudios">Optional filter whether to include studios.</param>
+ /// <param name="includeArtists">Optional filter whether to include artists.</param>
+ /// <response code="200">Search hint returned.</response>
+ /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
+ [HttpGet]
+ [Description("Gets search hints based on a search term")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<SearchHintResult> GetSearchHints(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] Guid? userId,
+ [FromQuery, Required] string searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool includePeople = true,
+ [FromQuery] bool includeMedia = true,
+ [FromQuery] bool includeGenres = true,
+ [FromQuery] bool includeStudios = true,
+ [FromQuery] bool includeArtists = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var result = _searchEngine.GetSearchHints(new SearchQuery
{
- _searchEngine = searchEngine;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _imageProcessor = imageProcessor;
- }
+ Limit = limit,
+ SearchTerm = searchTerm,
+ IncludeArtists = includeArtists,
+ IncludeGenres = includeGenres,
+ IncludeMedia = includeMedia,
+ IncludePeople = includePeople,
+ IncludeStudios = includeStudios,
+ StartIndex = startIndex,
+ UserId = userId.Value,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ MediaTypes = mediaTypes,
+ ParentId = parentId,
- /// <summary>
- /// Gets the search hint result.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
- /// <param name="searchTerm">The search term to filter on.</param>
- /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param>
- /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param>
- /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param>
- /// <param name="parentId">If specified, only children of the parent are returned.</param>
- /// <param name="isMovie">Optional filter for movies.</param>
- /// <param name="isSeries">Optional filter for series.</param>
- /// <param name="isNews">Optional filter for news.</param>
- /// <param name="isKids">Optional filter for kids.</param>
- /// <param name="isSports">Optional filter for sports.</param>
- /// <param name="includePeople">Optional filter whether to include people.</param>
- /// <param name="includeMedia">Optional filter whether to include media.</param>
- /// <param name="includeGenres">Optional filter whether to include genres.</param>
- /// <param name="includeStudios">Optional filter whether to include studios.</param>
- /// <param name="includeArtists">Optional filter whether to include artists.</param>
- /// <response code="200">Search hint returned.</response>
- /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
- [HttpGet]
- [Description("Gets search hints based on a search term")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<SearchHintResult> GetSearchHints(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] Guid? userId,
- [FromQuery, Required] string searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery] Guid? parentId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] bool includePeople = true,
- [FromQuery] bool includeMedia = true,
- [FromQuery] bool includeGenres = true,
- [FromQuery] bool includeStudios = true,
- [FromQuery] bool includeArtists = true)
- {
- var result = _searchEngine.GetSearchHints(new SearchQuery
- {
- Limit = limit,
- SearchTerm = searchTerm,
- IncludeArtists = includeArtists,
- IncludeGenres = includeGenres,
- IncludeMedia = includeMedia,
- IncludePeople = includePeople,
- IncludeStudios = includeStudios,
- StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
- IncludeItemTypes = includeItemTypes,
- ExcludeItemTypes = excludeItemTypes,
- MediaTypes = mediaTypes,
- ParentId = parentId,
+ IsKids = isKids,
+ IsMovie = isMovie,
+ IsNews = isNews,
+ IsSeries = isSeries,
+ IsSports = isSports
+ });
- IsKids = isKids,
- IsMovie = isMovie,
- IsNews = isNews,
- IsSeries = isSeries,
- IsSports = isSports
- });
+ return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount);
+ }
- return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount);
- }
+ /// <summary>
+ /// Gets the search hint result.
+ /// </summary>
+ /// <param name="hintInfo">The hint info.</param>
+ /// <returns>SearchHintResult.</returns>
+ private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
+ {
+ var item = hintInfo.Item;
- /// <summary>
- /// Gets the search hint result.
- /// </summary>
- /// <param name="hintInfo">The hint info.</param>
- /// <returns>SearchHintResult.</returns>
- private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
+ var result = new SearchHint
{
- var item = hintInfo.Item;
-
- var result = new SearchHint
- {
- Name = item.Name,
- IndexNumber = item.IndexNumber,
- ParentIndexNumber = item.ParentIndexNumber,
- Id = item.Id,
- Type = item.GetBaseItemKind(),
- MediaType = item.MediaType,
- MatchedTerm = hintInfo.MatchedTerm,
- RunTimeTicks = item.RunTimeTicks,
- ProductionYear = item.ProductionYear,
- ChannelId = item.ChannelId,
- EndDate = item.EndDate
- };
+ Name = item.Name,
+ IndexNumber = item.IndexNumber,
+ ParentIndexNumber = item.ParentIndexNumber,
+ Id = item.Id,
+ Type = item.GetBaseItemKind(),
+ MediaType = item.MediaType,
+ MatchedTerm = hintInfo.MatchedTerm,
+ RunTimeTicks = item.RunTimeTicks,
+ ProductionYear = item.ProductionYear,
+ ChannelId = item.ChannelId,
+ EndDate = item.EndDate
+ };
#pragma warning disable CS0618
- // Kept for compatibility with older clients
- result.ItemId = result.Id;
+ // Kept for compatibility with older clients
+ result.ItemId = result.Id;
#pragma warning restore CS0618
- if (item.IsFolder)
- {
- result.IsFolder = true;
- }
-
- var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
+ if (item.IsFolder)
+ {
+ result.IsFolder = true;
+ }
- if (primaryImageTag is not null)
- {
- result.PrimaryImageTag = primaryImageTag;
- result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
- }
+ var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
- SetThumbImageInfo(result, item);
- SetBackdropImageInfo(result, item);
+ if (primaryImageTag is not null)
+ {
+ result.PrimaryImageTag = primaryImageTag;
+ result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
+ }
- switch (item)
- {
- case IHasSeries hasSeries:
- result.Series = hasSeries.SeriesName;
- break;
- case LiveTvProgram program:
- result.StartDate = program.StartDate;
- break;
- case Series series:
- if (series.Status.HasValue)
- {
- result.Status = series.Status.Value.ToString();
- }
+ SetThumbImageInfo(result, item);
+ SetBackdropImageInfo(result, item);
- break;
- case MusicAlbum album:
- result.Artists = album.Artists;
- result.AlbumArtist = album.AlbumArtist;
- break;
- case Audio song:
- result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
- result.Artists = song.Artists;
+ switch (item)
+ {
+ case IHasSeries hasSeries:
+ result.Series = hasSeries.SeriesName;
+ break;
+ case LiveTvProgram program:
+ result.StartDate = program.StartDate;
+ break;
+ case Series series:
+ if (series.Status.HasValue)
+ {
+ result.Status = series.Status.Value.ToString();
+ }
- MusicAlbum musicAlbum = song.AlbumEntity;
+ break;
+ case MusicAlbum album:
+ result.Artists = album.Artists;
+ result.AlbumArtist = album.AlbumArtist;
+ break;
+ case Audio song:
+ result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
+ result.Artists = song.Artists;
- if (musicAlbum is not null)
- {
- result.Album = musicAlbum.Name;
- result.AlbumId = musicAlbum.Id;
- }
- else
- {
- result.Album = song.Album;
- }
+ MusicAlbum musicAlbum = song.AlbumEntity;
- break;
- }
+ if (musicAlbum is not null)
+ {
+ result.Album = musicAlbum.Name;
+ result.AlbumId = musicAlbum.Id;
+ }
+ else
+ {
+ result.Album = song.Album;
+ }
- if (!item.ChannelId.Equals(default))
- {
- var channel = _libraryManager.GetItemById(item.ChannelId);
- result.ChannelName = channel?.Name;
- }
+ break;
+ }
- return result;
+ if (!item.ChannelId.Equals(default))
+ {
+ var channel = _libraryManager.GetItemById(item.ChannelId);
+ result.ChannelName = channel?.Name;
}
- private void SetThumbImageInfo(SearchHint hint, BaseItem item)
+ return result;
+ }
+
+ private void SetThumbImageInfo(SearchHint hint, BaseItem item)
+ {
+ var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
+
+ if (itemWithImage is null && item is Episode)
{
- var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
+ itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
+ }
- if (itemWithImage is null && item is Episode)
- {
- itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
- }
+ itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb);
- itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb);
+ if (itemWithImage is not null)
+ {
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
- if (itemWithImage is not null)
+ if (tag is not null)
{
- var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
-
- if (tag is not null)
- {
- hint.ThumbImageTag = tag;
- hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
- }
+ hint.ThumbImageTag = tag;
+ hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
}
}
+ }
+
+ private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
+ {
+ var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
+ ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
- private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
+ if (itemWithImage is not null)
{
- var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
- ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
- if (itemWithImage is not null)
+ if (tag is not null)
{
- var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
-
- if (tag is not null)
- {
- hint.BackdropImageTag = tag;
- hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
- }
+ hint.BackdropImageTag = tag;
+ hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
}
}
+ }
- private T? GetParentWithImage<T>(BaseItem item, ImageType type)
- where T : BaseItem
- {
- return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
- }
+ private T? GetParentWithImage<T>(BaseItem item, ImageType type)
+ where T : BaseItem
+ {
+ return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
}
}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 25f930135..e93456de6 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -19,480 +19,483 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The session controller.
+/// </summary>
+[Route("")]
+public class SessionController : BaseJellyfinApiController
{
+ private readonly ISessionManager _sessionManager;
+ private readonly IUserManager _userManager;
+ private readonly IDeviceManager _deviceManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SessionController"/> class.
+ /// </summary>
+ /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+ /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+ public SessionController(
+ ISessionManager sessionManager,
+ IUserManager userManager,
+ IDeviceManager deviceManager)
+ {
+ _sessionManager = sessionManager;
+ _userManager = userManager;
+ _deviceManager = deviceManager;
+ }
+
/// <summary>
- /// The session controller.
+ /// Gets a list of sessions.
/// </summary>
- [Route("")]
- public class SessionController : BaseJellyfinApiController
+ /// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param>
+ /// <param name="deviceId">Filter by device Id.</param>
+ /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
+ /// <response code="200">List of sessions returned.</response>
+ /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
+ [HttpGet("Sessions")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<SessionInfo>> GetSessions(
+ [FromQuery] Guid? controllableByUserId,
+ [FromQuery] string? deviceId,
+ [FromQuery] int? activeWithinSeconds)
{
- private readonly ISessionManager _sessionManager;
- private readonly IUserManager _userManager;
- private readonly IDeviceManager _deviceManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SessionController"/> class.
- /// </summary>
- /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
- /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
- /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
- public SessionController(
- ISessionManager sessionManager,
- IUserManager userManager,
- IDeviceManager deviceManager)
+ var result = _sessionManager.Sessions;
+
+ if (!string.IsNullOrEmpty(deviceId))
{
- _sessionManager = sessionManager;
- _userManager = userManager;
- _deviceManager = deviceManager;
+ result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
}
- /// <summary>
- /// Gets a list of sessions.
- /// </summary>
- /// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param>
- /// <param name="deviceId">Filter by device Id.</param>
- /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
- /// <response code="200">List of sessions returned.</response>
- /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
- [HttpGet("Sessions")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<SessionInfo>> GetSessions(
- [FromQuery] Guid? controllableByUserId,
- [FromQuery] string? deviceId,
- [FromQuery] int? activeWithinSeconds)
+ if (controllableByUserId.HasValue && !controllableByUserId.Equals(default))
{
- var result = _sessionManager.Sessions;
+ result = result.Where(i => i.SupportsRemoteControl);
- if (!string.IsNullOrEmpty(deviceId))
+ var user = _userManager.GetUserById(controllableByUserId.Value);
+ if (user is null)
{
- result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
+ return NotFound();
}
- if (controllableByUserId.HasValue && !controllableByUserId.Equals(default))
+ if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
{
- result = result.Where(i => i.SupportsRemoteControl);
-
- var user = _userManager.GetUserById(controllableByUserId.Value);
-
- if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
- {
- result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value));
- }
+ result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value));
+ }
- if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
- {
- result = result.Where(i => !i.UserId.Equals(default));
- }
+ if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
+ {
+ result = result.Where(i => !i.UserId.Equals(default));
+ }
- if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
- {
- var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
- result = result.Where(i => i.LastActivityDate >= minActiveDate);
- }
+ if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
+ {
+ var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
+ result = result.Where(i => i.LastActivityDate >= minActiveDate);
+ }
- result = result.Where(i =>
+ result = result.Where(i =>
+ {
+ if (!string.IsNullOrWhiteSpace(i.DeviceId))
{
- if (!string.IsNullOrWhiteSpace(i.DeviceId))
+ if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
{
- if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
- {
- return false;
- }
+ return false;
}
+ }
- return true;
- });
- }
-
- return Ok(result);
+ return true;
+ });
}
- /// <summary>
- /// Instructs a session to browse to an item or view.
- /// </summary>
- /// <param name="sessionId">The session Id.</param>
- /// <param name="itemType">The type of item to browse to.</param>
- /// <param name="itemId">The Id of the item.</param>
- /// <param name="itemName">The name of the item.</param>
- /// <response code="204">Instruction sent to session.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/Viewing")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> DisplayContent(
- [FromRoute, Required] string sessionId,
- [FromQuery, Required] BaseItemKind itemType,
- [FromQuery, Required] string itemId,
- [FromQuery, Required] string itemName)
+ return Ok(result);
+ }
+
+ /// <summary>
+ /// Instructs a session to browse to an item or view.
+ /// </summary>
+ /// <param name="sessionId">The session Id.</param>
+ /// <param name="itemType">The type of item to browse to.</param>
+ /// <param name="itemId">The Id of the item.</param>
+ /// <param name="itemName">The name of the item.</param>
+ /// <response code="204">Instruction sent to session.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/{sessionId}/Viewing")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> DisplayContent(
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] BaseItemKind itemType,
+ [FromQuery, Required] string itemId,
+ [FromQuery, Required] string itemName)
+ {
+ var command = new BrowseRequest
{
- var command = new BrowseRequest
- {
- ItemId = itemId,
- ItemName = itemName,
- ItemType = itemType
- };
-
- await _sessionManager.SendBrowseCommand(
- await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
- sessionId,
- command,
- CancellationToken.None)
- .ConfigureAwait(false);
-
- return NoContent();
- }
+ ItemId = itemId,
+ ItemName = itemName,
+ ItemType = itemType
+ };
+
+ await _sessionManager.SendBrowseCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
+ sessionId,
+ command,
+ CancellationToken.None)
+ .ConfigureAwait(false);
+
+ return NoContent();
+ }
- /// <summary>
- /// Instructs a session to play an item.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <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")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> Play(
- [FromRoute, Required] string sessionId,
- [FromQuery, Required] PlayCommand playCommand,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
- [FromQuery] long? startPositionTicks,
- [FromQuery] string? mediaSourceId,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] int? startIndex)
+ /// <summary>
+ /// Instructs a session to play an item.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <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")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> Play(
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] PlayCommand playCommand,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
+ [FromQuery] long? startPositionTicks,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? startIndex)
+ {
+ var playRequest = new PlayRequest
{
- var playRequest = new PlayRequest
+ ItemIds = itemIds,
+ StartPositionTicks = startPositionTicks,
+ PlayCommand = playCommand,
+ MediaSourceId = mediaSourceId,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ StartIndex = startIndex
+ };
+
+ await _sessionManager.SendPlayCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
+ sessionId,
+ playRequest,
+ CancellationToken.None)
+ .ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Issues a playstate command to a client.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The <see cref="PlaystateCommand"/>.</param>
+ /// <param name="seekPositionTicks">The optional position ticks.</param>
+ /// <param name="controllingUserId">The optional controlling user id.</param>
+ /// <response code="204">Playstate command sent to session.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/{sessionId}/Playing/{command}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> SendPlaystateCommand(
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] PlaystateCommand command,
+ [FromQuery] long? seekPositionTicks,
+ [FromQuery] string? controllingUserId)
+ {
+ await _sessionManager.SendPlaystateCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
+ sessionId,
+ new PlaystateRequest()
{
- ItemIds = itemIds,
- StartPositionTicks = startPositionTicks,
- PlayCommand = playCommand,
- MediaSourceId = mediaSourceId,
- AudioStreamIndex = audioStreamIndex,
- SubtitleStreamIndex = subtitleStreamIndex,
- StartIndex = startIndex
- };
-
- await _sessionManager.SendPlayCommand(
- await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
- sessionId,
- playRequest,
- CancellationToken.None)
- .ConfigureAwait(false);
-
- return NoContent();
- }
+ Command = command,
+ ControllingUserId = controllingUserId,
+ SeekPositionTicks = seekPositionTicks,
+ },
+ CancellationToken.None)
+ .ConfigureAwait(false);
+
+ return NoContent();
+ }
- /// <summary>
- /// Issues a playstate command to a client.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="command">The <see cref="PlaystateCommand"/>.</param>
- /// <param name="seekPositionTicks">The optional position ticks.</param>
- /// <param name="controllingUserId">The optional controlling user id.</param>
- /// <response code="204">Playstate command sent to session.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/Playing/{command}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> SendPlaystateCommand(
- [FromRoute, Required] string sessionId,
- [FromRoute, Required] PlaystateCommand command,
- [FromQuery] long? seekPositionTicks,
- [FromQuery] string? controllingUserId)
+ /// <summary>
+ /// Issues a system command to a client.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The command to send.</param>
+ /// <response code="204">System command sent to session.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/{sessionId}/System/{command}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> SendSystemCommand(
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] GeneralCommandType command)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var generalCommand = new GeneralCommand
{
- await _sessionManager.SendPlaystateCommand(
- await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
- sessionId,
- new PlaystateRequest()
- {
- Command = command,
- ControllingUserId = controllingUserId,
- SeekPositionTicks = seekPositionTicks,
- },
- CancellationToken.None)
- .ConfigureAwait(false);
-
- return NoContent();
- }
+ Name = command,
+ ControllingUserId = currentSession.UserId
+ };
- /// <summary>
- /// Issues a system command to a client.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="command">The command to send.</param>
- /// <response code="204">System command sent to session.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/System/{command}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> SendSystemCommand(
- [FromRoute, Required] string sessionId,
- [FromRoute, Required] GeneralCommandType command)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var generalCommand = new GeneralCommand
- {
- Name = command,
- ControllingUserId = currentSession.UserId
- };
+ await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false);
- await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Issues a general command to a client.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The command to send.</param>
+ /// <response code="204">General command sent to session.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/{sessionId}/Command/{command}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> SendGeneralCommand(
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] GeneralCommandType command)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- /// <summary>
- /// Issues a general command to a client.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="command">The command to send.</param>
- /// <response code="204">General command sent to session.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/Command/{command}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> SendGeneralCommand(
- [FromRoute, Required] string sessionId,
- [FromRoute, Required] GeneralCommandType command)
+ var generalCommand = new GeneralCommand
{
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ Name = command,
+ ControllingUserId = currentSession.UserId
+ };
- var generalCommand = new GeneralCommand
- {
- Name = command,
- ControllingUserId = currentSession.UserId
- };
+ await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
+ .ConfigureAwait(false);
- await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Issues a full general command to a client.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The <see cref="GeneralCommand"/>.</param>
+ /// <response code="204">Full general command sent to session.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/{sessionId}/Command")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> SendFullGeneralCommand(
+ [FromRoute, Required] string sessionId,
+ [FromBody, Required] GeneralCommand command)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- /// <summary>
- /// Issues a full general command to a client.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="command">The <see cref="GeneralCommand"/>.</param>
- /// <response code="204">Full general command sent to session.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/Command")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> SendFullGeneralCommand(
- [FromRoute, Required] string sessionId,
- [FromBody, Required] GeneralCommand command)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ ArgumentNullException.ThrowIfNull(command);
- ArgumentNullException.ThrowIfNull(command);
+ command.ControllingUserId = currentSession.UserId;
- command.ControllingUserId = currentSession.UserId;
+ await _sessionManager.SendGeneralCommand(
+ currentSession.Id,
+ sessionId,
+ command,
+ CancellationToken.None)
+ .ConfigureAwait(false);
- await _sessionManager.SendGeneralCommand(
- currentSession.Id,
- sessionId,
- command,
- CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
+ /// <summary>
+ /// Issues a command to a client to display a message to the user.
+ /// </summary>
+ /// <param name="sessionId">The session id.</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")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> SendMessageCommand(
+ [FromRoute, Required] string sessionId,
+ [FromBody, Required] MessageCommand command)
+ {
+ if (string.IsNullOrWhiteSpace(command.Header))
+ {
+ command.Header = "Message from Server";
}
- /// <summary>
- /// Issues a command to a client to display a message to the user.
- /// </summary>
- /// <param name="sessionId">The session id.</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")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> SendMessageCommand(
- [FromRoute, Required] string sessionId,
- [FromBody, Required] MessageCommand command)
- {
- if (string.IsNullOrWhiteSpace(command.Header))
- {
- command.Header = "Message from Server";
- }
+ await _sessionManager.SendMessageCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
+ sessionId,
+ command,
+ CancellationToken.None)
+ .ConfigureAwait(false);
- await _sessionManager.SendMessageCommand(
- await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
- sessionId,
- command,
- CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Adds an additional user to a session.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="204">User added to session.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/{sessionId}/User/{userId}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult AddUserToSession(
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
+ {
+ _sessionManager.AddAdditionalUser(sessionId, userId);
+ return NoContent();
+ }
- /// <summary>
- /// Adds an additional user to a session.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="userId">The user id.</param>
- /// <response code="204">User added to session.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/User/{userId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult AddUserToSession(
- [FromRoute, Required] string sessionId,
- [FromRoute, Required] Guid userId)
- {
- _sessionManager.AddAdditionalUser(sessionId, userId);
- return NoContent();
- }
+ /// <summary>
+ /// Removes an additional user from a session.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="204">User removed from session.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Sessions/{sessionId}/User/{userId}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult RemoveUserFromSession(
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
+ {
+ _sessionManager.RemoveAdditionalUser(sessionId, userId);
+ return NoContent();
+ }
- /// <summary>
- /// Removes an additional user from a session.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="userId">The user id.</param>
- /// <response code="204">User removed from session.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Sessions/{sessionId}/User/{userId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult RemoveUserFromSession(
- [FromRoute, Required] string sessionId,
- [FromRoute, Required] Guid userId)
+ /// <summary>
+ /// Updates capabilities for a device.
+ /// </summary>
+ /// <param name="id">The session id.</param>
+ /// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param>
+ /// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param>
+ /// <param name="supportsMediaControl">Determines whether media can be played remotely..</param>
+ /// <param name="supportsSync">Determines whether sync is supported.</param>
+ /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param>
+ /// <response code="204">Capabilities posted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Capabilities")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> PostCapabilities(
+ [FromQuery] string? id,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
+ [FromQuery] bool supportsMediaControl = false,
+ [FromQuery] bool supportsSync = false,
+ [FromQuery] bool supportsPersistentIdentifier = true)
+ {
+ if (string.IsNullOrWhiteSpace(id))
{
- _sessionManager.RemoveAdditionalUser(sessionId, userId);
- return NoContent();
+ id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
}
- /// <summary>
- /// Updates capabilities for a device.
- /// </summary>
- /// <param name="id">The session id.</param>
- /// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param>
- /// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param>
- /// <param name="supportsMediaControl">Determines whether media can be played remotely..</param>
- /// <param name="supportsSync">Determines whether sync is supported.</param>
- /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param>
- /// <response code="204">Capabilities posted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Capabilities")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> PostCapabilities(
- [FromQuery] string? id,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
- [FromQuery] bool supportsMediaControl = false,
- [FromQuery] bool supportsSync = false,
- [FromQuery] bool supportsPersistentIdentifier = true)
+ _sessionManager.ReportCapabilities(id, new ClientCapabilities
{
- if (string.IsNullOrWhiteSpace(id))
- {
- id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- }
-
- _sessionManager.ReportCapabilities(id, new ClientCapabilities
- {
- PlayableMediaTypes = playableMediaTypes,
- SupportedCommands = supportedCommands,
- SupportsMediaControl = supportsMediaControl,
- SupportsSync = supportsSync,
- SupportsPersistentIdentifier = supportsPersistentIdentifier
- });
- return NoContent();
- }
+ PlayableMediaTypes = playableMediaTypes,
+ SupportedCommands = supportedCommands,
+ SupportsMediaControl = supportsMediaControl,
+ SupportsSync = supportsSync,
+ SupportsPersistentIdentifier = supportsPersistentIdentifier
+ });
+ return NoContent();
+ }
- /// <summary>
- /// Updates capabilities for a device.
- /// </summary>
- /// <param name="id">The session id.</param>
- /// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param>
- /// <response code="204">Capabilities updated.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Capabilities/Full")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> PostFullCapabilities(
- [FromQuery] string? id,
- [FromBody, Required] ClientCapabilitiesDto capabilities)
+ /// <summary>
+ /// Updates capabilities for a device.
+ /// </summary>
+ /// <param name="id">The session id.</param>
+ /// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param>
+ /// <response code="204">Capabilities updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Capabilities/Full")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> PostFullCapabilities(
+ [FromQuery] string? id,
+ [FromBody, Required] ClientCapabilitiesDto capabilities)
+ {
+ if (string.IsNullOrWhiteSpace(id))
{
- if (string.IsNullOrWhiteSpace(id))
- {
- id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- }
+ id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ }
- _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
+ _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
- return NoContent();
- }
+ return NoContent();
+ }
- /// <summary>
- /// Reports that a session is viewing an item.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="itemId">The item id.</param>
- /// <response code="204">Session reported to server.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Viewing")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> ReportViewing(
- [FromQuery] string? sessionId,
- [FromQuery, Required] string? itemId)
- {
- string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ /// <summary>
+ /// Reports that a session is viewing an item.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="204">Session reported to server.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Viewing")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> ReportViewing(
+ [FromQuery] string? sessionId,
+ [FromQuery, Required] string? itemId)
+ {
+ string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- _sessionManager.ReportNowViewingItem(session, itemId);
- return NoContent();
- }
+ _sessionManager.ReportNowViewingItem(session, itemId);
+ return NoContent();
+ }
- /// <summary>
- /// Reports that a session has ended.
- /// </summary>
- /// <response code="204">Session end reported to server.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Logout")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> ReportSessionEnded()
- {
- await _sessionManager.Logout(User.GetToken()).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Reports that a session has ended.
+ /// </summary>
+ /// <response code="204">Session end reported to server.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/Logout")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> ReportSessionEnded()
+ {
+ await _sessionManager.Logout(User.GetToken()).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Get all auth providers.
- /// </summary>
- /// <response code="200">Auth providers retrieved.</response>
- /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
- [HttpGet("Auth/Providers")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
- {
- return _userManager.GetAuthenticationProviders();
- }
+ /// <summary>
+ /// Get all auth providers.
+ /// </summary>
+ /// <response code="200">Auth providers retrieved.</response>
+ /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
+ [HttpGet("Auth/Providers")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
+ {
+ return _userManager.GetAuthenticationProviders();
+ }
- /// <summary>
- /// Get all password reset providers.
- /// </summary>
- /// <response code="200">Password reset providers retrieved.</response>
- /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
- [HttpGet("Auth/PasswordResetProviders")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.RequiresElevation)]
- public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
- {
- return _userManager.GetPasswordResetProviders();
- }
+ /// <summary>
+ /// Get all password reset providers.
+ /// </summary>
+ /// <response code="200">Password reset providers retrieved.</response>
+ /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
+ [HttpGet("Auth/PasswordResetProviders")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
+ {
+ return _userManager.GetPasswordResetProviders();
}
}
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index eec5779e6..aab390d1f 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -10,141 +10,140 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The startup wizard controller.
+/// </summary>
+[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+public class StartupController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _config;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// The startup wizard controller.
+ /// Initializes a new instance of the <see cref="StartupController" /> class.
/// </summary>
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- public class StartupController : BaseJellyfinApiController
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="userManager">The user manager.</param>
+ public StartupController(IServerConfigurationManager config, IUserManager userManager)
{
- private readonly IServerConfigurationManager _config;
- private readonly IUserManager _userManager;
+ _config = config;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="StartupController" /> class.
- /// </summary>
- /// <param name="config">The server configuration manager.</param>
- /// <param name="userManager">The user manager.</param>
- public StartupController(IServerConfigurationManager config, IUserManager userManager)
- {
- _config = config;
- _userManager = userManager;
- }
+ /// <summary>
+ /// Completes the startup wizard.
+ /// </summary>
+ /// <response code="204">Startup wizard completed.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Complete")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CompleteWizard()
+ {
+ _config.Configuration.IsStartupWizardCompleted = true;
+ _config.SaveConfiguration();
+ return NoContent();
+ }
- /// <summary>
- /// Completes the startup wizard.
- /// </summary>
- /// <response code="204">Startup wizard completed.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Complete")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CompleteWizard()
+ /// <summary>
+ /// Gets the initial startup wizard configuration.
+ /// </summary>
+ /// <response code="200">Initial startup wizard configuration retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
+ [HttpGet("Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
+ {
+ return new StartupConfigurationDto
{
- _config.Configuration.IsStartupWizardCompleted = true;
- _config.SaveConfiguration();
- return NoContent();
- }
+ UICulture = _config.Configuration.UICulture,
+ MetadataCountryCode = _config.Configuration.MetadataCountryCode,
+ PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
+ };
+ }
- /// <summary>
- /// Gets the initial startup wizard configuration.
- /// </summary>
- /// <response code="200">Initial startup wizard configuration retrieved.</response>
- /// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
- [HttpGet("Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
- {
- return new StartupConfigurationDto
- {
- UICulture = _config.Configuration.UICulture,
- MetadataCountryCode = _config.Configuration.MetadataCountryCode,
- PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
- };
- }
+ /// <summary>
+ /// Sets the initial startup wizard configuration.
+ /// </summary>
+ /// <param name="startupConfiguration">The updated startup configuration.</param>
+ /// <response code="204">Configuration saved.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Configuration")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
+ {
+ _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
+ _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
+ _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
+ _config.SaveConfiguration();
+ return NoContent();
+ }
- /// <summary>
- /// Sets the initial startup wizard configuration.
- /// </summary>
- /// <param name="startupConfiguration">The updated startup configuration.</param>
- /// <response code="204">Configuration saved.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Configuration")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
- {
- _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
- _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
- _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
- _config.SaveConfiguration();
- return NoContent();
- }
+ /// <summary>
+ /// Sets remote access and UPnP.
+ /// </summary>
+ /// <param name="startupRemoteAccessDto">The startup remote access dto.</param>
+ /// <response code="204">Configuration saved.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("RemoteAccess")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
+ {
+ NetworkConfiguration settings = _config.GetNetworkConfiguration();
+ settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
+ settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
+ _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
+ return NoContent();
+ }
- /// <summary>
- /// Sets remote access and UPnP.
- /// </summary>
- /// <param name="startupRemoteAccessDto">The startup remote access dto.</param>
- /// <response code="204">Configuration saved.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("RemoteAccess")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
+ /// <summary>
+ /// Gets the first user.
+ /// </summary>
+ /// <response code="200">Initial user retrieved.</response>
+ /// <returns>The first user.</returns>
+ [HttpGet("User")]
+ [HttpGet("FirstUser", Name = "GetFirstUser_2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<StartupUserDto> GetFirstUser()
+ {
+ // TODO: Remove this method when startup wizard no longer requires an existing user.
+ await _userManager.InitializeAsync().ConfigureAwait(false);
+ var user = _userManager.Users.First();
+ return new StartupUserDto
{
- NetworkConfiguration settings = _config.GetNetworkConfiguration();
- settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
- settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
- _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
- return NoContent();
- }
+ Name = user.Username,
+ Password = user.Password
+ };
+ }
- /// <summary>
- /// Gets the first user.
- /// </summary>
- /// <response code="200">Initial user retrieved.</response>
- /// <returns>The first user.</returns>
- [HttpGet("User")]
- [HttpGet("FirstUser", Name = "GetFirstUser_2")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<StartupUserDto> GetFirstUser()
- {
- // TODO: Remove this method when startup wizard no longer requires an existing user.
- await _userManager.InitializeAsync().ConfigureAwait(false);
- var user = _userManager.Users.First();
- return new StartupUserDto
- {
- Name = user.Username,
- Password = user.Password
- };
- }
+ /// <summary>
+ /// Sets the user name and password.
+ /// </summary>
+ /// <param name="startupUserDto">The DTO containing username and password.</param>
+ /// <response code="204">Updated user name and password.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous update operation.
+ /// The task result contains a <see cref="NoContentResult"/> indicating success.
+ /// </returns>
+ [HttpPost("User")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
+ {
+ var user = _userManager.Users.First();
- /// <summary>
- /// Sets the user name and password.
- /// </summary>
- /// <param name="startupUserDto">The DTO containing username and password.</param>
- /// <response code="204">Updated user name and password.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous update operation.
- /// The task result contains a <see cref="NoContentResult"/> indicating success.
- /// </returns>
- [HttpPost("User")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
+ if (startupUserDto.Name is not null)
{
- var user = _userManager.Users.First();
-
- if (startupUserDto.Name is not null)
- {
- user.Username = startupUserDto.Name;
- }
-
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ user.Username = startupUserDto.Name;
+ }
- if (!string.IsNullOrEmpty(startupUserDto.Password))
- {
- await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
- }
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- return NoContent();
+ if (!string.IsNullOrEmpty(startupUserDto.Password))
+ {
+ await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
}
+
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 1288fb512..f434f60f5 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -16,141 +15,142 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Studios controller.
+/// </summary>
+[Authorize]
+public class StudiosController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// Studios controller.
+ /// Initializes a new instance of the <see cref="StudiosController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class StudiosController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public StudiosController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="StudiosController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public StudiosController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- }
+ /// <summary>
+ /// Gets all studios from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">Optional. Search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
+ /// <param name="userId">User id.</param>
+ /// <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="enableImages">Optional, include image information in output.</param>
+ /// <param name="enableTotalRecordCount">Total record count.</param>
+ /// <response code="200">Studios returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the studios.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetStudios(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- /// <summary>
- /// Gets all studios from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">Optional. Search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
- /// <param name="userId">User id.</param>
- /// <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="enableImages">Optional, include image information in output.</param>
- /// <param name="enableTotalRecordCount">Total record count.</param>
- /// <response code="200">Studios returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the studios.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetStudios(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isFavorite,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ User? user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
- var parentItem = _libraryManager.GetParentItem(parentId, userId);
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
- var query = new InternalItemsQuery(user)
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
- };
-
- if (parentId.HasValue)
+ query.AncestorIds = new[] { parentId.Value };
+ }
+ else
{
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { parentId.Value };
- }
- else
- {
- query.ItemIds = new[] { parentId.Value };
- }
+ query.ItemIds = new[] { parentId.Value };
}
-
- var result = _libraryManager.GetStudios(query);
- var shouldIncludeItemTypes = includeItemTypes.Length != 0;
- return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
- /// <summary>
- /// Gets a studio by name.
- /// </summary>
- /// <param name="name">Studio name.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Studio returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the studio.</returns>
- [HttpGet("{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
- {
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ var result = _libraryManager.GetStudios(query);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
+ }
- var item = _libraryManager.GetStudio(name);
- if (userId.HasValue && !userId.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
+ /// <summary>
+ /// Gets a studio by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Studio returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the studio.</returns>
+ [HttpGet("{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ var item = _libraryManager.GetStudio(name);
+ if (!userId.Equals(default))
+ {
+ var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index c3ce1868e..e38421338 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -30,522 +30,521 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Subtitle controller.
+/// </summary>
+[Route("")]
+public class SubtitleController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ISubtitleManager _subtitleManager;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<SubtitleController> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SubtitleController"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
+ /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
+ public SubtitleController(
+ IServerConfigurationManager serverConfigurationManager,
+ ILibraryManager libraryManager,
+ ISubtitleManager subtitleManager,
+ ISubtitleEncoder subtitleEncoder,
+ IMediaSourceManager mediaSourceManager,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILogger<SubtitleController> logger)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ _libraryManager = libraryManager;
+ _subtitleManager = subtitleManager;
+ _subtitleEncoder = subtitleEncoder;
+ _mediaSourceManager = mediaSourceManager;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
/// <summary>
- /// Subtitle controller.
+ /// Deletes an external subtitle file.
/// </summary>
- [Route("")]
- public class SubtitleController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="index">The index of the subtitle file.</param>
+ /// <response code="204">Subtitle deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Videos/{itemId}/Subtitles/{index}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<Task> DeleteSubtitle(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int index)
{
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly ILibraryManager _libraryManager;
- private readonly ISubtitleManager _subtitleManager;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<SubtitleController> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SubtitleController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
- /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
- public SubtitleController(
- IServerConfigurationManager serverConfigurationManager,
- ILibraryManager libraryManager,
- ISubtitleManager subtitleManager,
- ISubtitleEncoder subtitleEncoder,
- IMediaSourceManager mediaSourceManager,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILogger<SubtitleController> logger)
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item is null)
{
- _serverConfigurationManager = serverConfigurationManager;
- _libraryManager = libraryManager;
- _subtitleManager = subtitleManager;
- _subtitleEncoder = subtitleEncoder;
- _mediaSourceManager = mediaSourceManager;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- _logger = logger;
+ return NotFound();
}
- /// <summary>
- /// Deletes an external subtitle file.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="index">The index of the subtitle file.</param>
- /// <response code="204">Subtitle deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Videos/{itemId}/Subtitles/{index}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<Task> DeleteSubtitle(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] int index)
- {
- var item = _libraryManager.GetItemById(itemId);
+ _subtitleManager.DeleteSubtitles(item, index);
+ return NoContent();
+ }
- if (item is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Search remote subtitles.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="language">The language of the subtitles.</param>
+ /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
+ /// <response code="200">Subtitles retrieved.</response>
+ /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
+ [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string language,
+ [FromQuery] bool? isPerfectMatch)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
- _subtitleManager.DeleteSubtitles(item, index);
- return NoContent();
- }
+ return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Downloads a remote subtitle.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="subtitleId">The subtitle id.</param>
+ /// <response code="204">Subtitle downloaded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> DownloadRemoteSubtitles(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string subtitleId)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
- /// <summary>
- /// Search remote subtitles.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="language">The language of the subtitles.</param>
- /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
- /// <response code="200">Subtitles retrieved.</response>
- /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
- [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string language,
- [FromQuery] bool? isPerfectMatch)
+ try
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+ .ConfigureAwait(false);
- return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
}
-
- /// <summary>
- /// Downloads a remote subtitle.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="subtitleId">The subtitle id.</param>
- /// <response code="204">Subtitle downloaded.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> DownloadRemoteSubtitles(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string subtitleId)
+ catch (Exception ex)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ _logger.LogError(ex, "Error downloading subtitles");
+ }
- try
- {
- await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error downloading subtitles");
- }
+ /// <summary>
+ /// Gets the remote subtitles.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <response code="200">File returned.</response>
+ /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
+ [HttpGet("Providers/Subtitles/Subtitles/{id}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesFile("text/*")]
+ public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id)
+ {
+ var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
+ }
- /// <summary>
- /// Gets the remote subtitles.
- /// </summary>
- /// <param name="id">The item id.</param>
- /// <response code="200">File returned.</response>
- /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
- [HttpGet("Providers/Subtitles/Subtitles/{id}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Produces(MediaTypeNames.Application.Octet)]
- [ProducesFile("text/*")]
- public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id)
- {
- var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
+ /// <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>
+ /// <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>
+ /// <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/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile("text/*")]
+ public async Task<ActionResult> GetSubtitle(
+ [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;
- return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
+ if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
+ {
+ format = "json";
}
- /// <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>
- /// <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>
- /// <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/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile("text/*")]
- public async Task<ActionResult> GetSubtitle(
- [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)
+ if (string.IsNullOrEmpty(format))
{
- // 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";
- }
-
- if (string.IsNullOrEmpty(format))
- {
- var item = (Video)_libraryManager.GetItemById(itemId.Value);
+ var item = (Video)_libraryManager.GetItemById(itemId.Value);
- var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
- var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
- .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
+ var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
+ var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
+ .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
- var subtitleStream = mediaSource.MediaStreams
- .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
+ var subtitleStream = mediaSource.MediaStreams
+ .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
- return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
- }
+ return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
+ }
- if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
+ if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
+ {
+ Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
- await using (stream.ConfigureAwait(false))
- {
- using var reader = new StreamReader(stream);
+ using var reader = new StreamReader(stream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+ var text = await reader.ReadToEndAsync().ConfigureAwait(false);
- text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
+ text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
- return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
- }
+ return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
}
-
- return File(
- await EncodeSubtitles(
- itemId.Value,
- mediaSourceId,
- index.Value,
- format,
- startPositionTicks,
- endPositionTicks,
- copyTimestamps).ConfigureAwait(false),
- MimeTypes.GetMimeType("file." + format));
}
- /// <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">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/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile("text/*")]
- public Task<ActionResult> GetSubtitleWithTicks(
- [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,
+ return File(
+ await EncodeSubtitles(
+ itemId.Value,
mediaSourceId,
- index,
+ index.Value,
format,
+ startPositionTicks,
endPositionTicks,
- copyTimestamps,
- addVttTimeMap,
- startPositionTicks ?? routeStartPositionTicks);
- }
+ copyTimestamps).ConfigureAwait(false),
+ MimeTypes.GetMimeType("file." + format));
+ }
- /// <summary>
- /// Gets an HLS subtitle playlist.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="index">The subtitle stream index.</param>
- /// <param name="mediaSourceId">The media source id.</param>
- /// <param name="segmentLength">The subtitle segment length.</param>
- /// <response code="200">Subtitle playlist retrieved.</response>
- /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
- [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> GetSubtitlePlaylist(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] int index,
- [FromRoute, Required] string mediaSourceId,
- [FromQuery, Required] int segmentLength)
- {
- var item = (Video)_libraryManager.GetItemById(itemId);
+ /// <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">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/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile("text/*")]
+ public Task<ActionResult> GetSubtitleWithTicks(
+ [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,
+ format,
+ endPositionTicks,
+ copyTimestamps,
+ addVttTimeMap,
+ startPositionTicks ?? routeStartPositionTicks);
+ }
- var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
+ /// <summary>
+ /// Gets an HLS subtitle playlist.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="index">The subtitle stream index.</param>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <param name="segmentLength">The subtitle segment length.</param>
+ /// <response code="200">Subtitle playlist retrieved.</response>
+ /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
+ [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetSubtitlePlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int index,
+ [FromRoute, Required] string mediaSourceId,
+ [FromQuery, Required] int segmentLength)
+ {
+ var item = (Video)_libraryManager.GetItemById(itemId);
- var runtime = mediaSource.RunTimeTicks ?? -1;
+ var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
- if (runtime <= 0)
- {
- throw new ArgumentException("HLS Subtitles are not supported for this media.");
- }
+ var runtime = mediaSource.RunTimeTicks ?? -1;
- var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
- if (segmentLengthTicks <= 0)
- {
- throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
- }
+ if (runtime <= 0)
+ {
+ throw new ArgumentException("HLS Subtitles are not supported for this media.");
+ }
- var builder = new StringBuilder();
- builder.AppendLine("#EXTM3U")
- .Append("#EXT-X-TARGETDURATION:")
- .Append(segmentLength)
- .AppendLine()
- .AppendLine("#EXT-X-VERSION:3")
- .AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
- .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+ var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
+ if (segmentLengthTicks <= 0)
+ {
+ throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
+ }
- long positionTicks = 0;
+ var builder = new StringBuilder();
+ builder.AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .Append(segmentLength)
+ .AppendLine()
+ .AppendLine("#EXT-X-VERSION:3")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
- var accessToken = User.GetToken();
+ long positionTicks = 0;
- while (positionTicks < runtime)
- {
- var remaining = runtime - positionTicks;
- var lengthTicks = Math.Min(remaining, segmentLengthTicks);
+ var accessToken = User.GetToken();
- builder.Append("#EXTINF:")
- .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
- .Append(',')
- .AppendLine();
+ while (positionTicks < runtime)
+ {
+ var remaining = runtime - positionTicks;
+ var lengthTicks = Math.Min(remaining, segmentLengthTicks);
- var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
+ builder.Append("#EXTINF:")
+ .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
+ .Append(',')
+ .AppendLine();
- var url = string.Format(
- CultureInfo.InvariantCulture,
- "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
- positionTicks.ToString(CultureInfo.InvariantCulture),
- endPositionTicks.ToString(CultureInfo.InvariantCulture),
- accessToken);
+ var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
- builder.AppendLine(url);
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
+ positionTicks.ToString(CultureInfo.InvariantCulture),
+ endPositionTicks.ToString(CultureInfo.InvariantCulture),
+ accessToken);
- positionTicks += segmentLengthTicks;
- }
+ builder.AppendLine(url);
- builder.AppendLine("#EXT-X-ENDLIST");
- return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ positionTicks += segmentLengthTicks;
}
- /// <summary>
- /// Upload an external subtitle file.
- /// </summary>
- /// <param name="itemId">The item the subtitle belongs to.</param>
- /// <param name="body">The request body.</param>
- /// <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,
- [FromBody, Required] UploadSubtitleDto body)
- {
- var video = (Video)_libraryManager.GetItemById(itemId);
- var data = Convert.FromBase64String(body.Data);
- var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
- await using (memoryStream.ConfigureAwait(false))
- {
- await _subtitleManager.UploadSubtitle(
- video,
- new SubtitleResponse
- {
- Format = body.Format,
- Language = body.Language,
- IsForced = body.IsForced,
- Stream = memoryStream
- }).ConfigureAwait(false);
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
-
- return NoContent();
- }
- }
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- /// <summary>
- /// Encodes a subtitle in the specified format.
- /// </summary>
- /// <param name="id">The media id.</param>
- /// <param name="mediaSourceId">The source media id.</param>
- /// <param name="index">The subtitle index.</param>
- /// <param name="format">The format to convert to.</param>
- /// <param name="startPositionTicks">The start position in ticks.</param>
- /// <param name="endPositionTicks">The end position in ticks.</param>
- /// <param name="copyTimestamps">Whether to copy the timestamps.</param>
- /// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns>
- private Task<Stream> EncodeSubtitles(
- Guid id,
- string? mediaSourceId,
- int index,
- string format,
- long startPositionTicks,
- long? endPositionTicks,
- bool copyTimestamps)
+ /// <summary>
+ /// Upload an external subtitle file.
+ /// </summary>
+ /// <param name="itemId">The item the subtitle belongs to.</param>
+ /// <param name="body">The request body.</param>
+ /// <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,
+ [FromBody, Required] UploadSubtitleDto body)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
+ var data = Convert.FromBase64String(body.Data);
+ var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
+ await using (memoryStream.ConfigureAwait(false))
{
- var item = _libraryManager.GetItemById(id);
+ await _subtitleManager.UploadSubtitle(
+ video,
+ new SubtitleResponse
+ {
+ Format = body.Format,
+ Language = body.Language,
+ IsForced = body.IsForced,
+ Stream = memoryStream
+ }).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
- return _subtitleEncoder.GetSubtitles(
- item,
- mediaSourceId,
- index,
- format,
- startPositionTicks,
- endPositionTicks ?? 0,
- copyTimestamps,
- CancellationToken.None);
+ return NoContent();
}
+ }
- /// <summary>
- /// Gets a list of available fallback font files.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
- [HttpGet("FallbackFont/Fonts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<FontFile> GetFallbackFontList()
- {
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var fallbackFontPath = encodingOptions.FallbackFontPath;
+ /// <summary>
+ /// Encodes a subtitle in the specified format.
+ /// </summary>
+ /// <param name="id">The media id.</param>
+ /// <param name="mediaSourceId">The source media id.</param>
+ /// <param name="index">The subtitle index.</param>
+ /// <param name="format">The format to convert to.</param>
+ /// <param name="startPositionTicks">The start position in ticks.</param>
+ /// <param name="endPositionTicks">The end position in ticks.</param>
+ /// <param name="copyTimestamps">Whether to copy the timestamps.</param>
+ /// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns>
+ private Task<Stream> EncodeSubtitles(
+ Guid id,
+ string? mediaSourceId,
+ int index,
+ string format,
+ long startPositionTicks,
+ long? endPositionTicks,
+ bool copyTimestamps)
+ {
+ var item = _libraryManager.GetItemById(id);
+
+ return _subtitleEncoder.GetSubtitles(
+ item,
+ mediaSourceId,
+ index,
+ format,
+ startPositionTicks,
+ endPositionTicks ?? 0,
+ copyTimestamps,
+ CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Gets a list of available fallback font files.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
+ [HttpGet("FallbackFont/Fonts")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<FontFile> GetFallbackFontList()
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var fallbackFontPath = encodingOptions.FallbackFontPath;
- if (!string.IsNullOrEmpty(fallbackFontPath))
+ if (!string.IsNullOrEmpty(fallbackFontPath))
+ {
+ var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
+ var fontFiles = files
+ .Select(i => new FontFile
+ {
+ Name = i.Name,
+ Size = i.Length,
+ DateCreated = _fileSystem.GetCreationTimeUtc(i),
+ DateModified = _fileSystem.GetLastWriteTimeUtc(i)
+ })
+ .OrderBy(i => i.Size)
+ .ThenBy(i => i.Name)
+ .ThenByDescending(i => i.DateModified)
+ .ThenByDescending(i => i.DateCreated);
+ // max total size 20M
+ const int MaxSize = 20971520;
+ var sizeCounter = 0L;
+ foreach (var fontFile in fontFiles)
{
- var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
- var fontFiles = files
- .Select(i => new FontFile
- {
- Name = i.Name,
- Size = i.Length,
- DateCreated = _fileSystem.GetCreationTimeUtc(i),
- DateModified = _fileSystem.GetLastWriteTimeUtc(i)
- })
- .OrderBy(i => i.Size)
- .ThenBy(i => i.Name)
- .ThenByDescending(i => i.DateModified)
- .ThenByDescending(i => i.DateCreated);
- // max total size 20M
- const int MaxSize = 20971520;
- var sizeCounter = 0L;
- foreach (var fontFile in fontFiles)
+ sizeCounter += fontFile.Size;
+ if (sizeCounter >= MaxSize)
{
- sizeCounter += fontFile.Size;
- if (sizeCounter >= MaxSize)
- {
- _logger.LogWarning("Some fonts will not be sent due to size limitations");
- yield break;
- }
-
- yield return fontFile;
+ _logger.LogWarning("Some fonts will not be sent due to size limitations");
+ yield break;
}
+
+ yield return fontFile;
}
- else
- {
- _logger.LogWarning("The path of fallback font folder has not been set");
- encodingOptions.EnableFallbackFont = false;
- }
}
+ else
+ {
+ _logger.LogWarning("The path of fallback font folder has not been set");
+ encodingOptions.EnableFallbackFont = false;
+ }
+ }
- /// <summary>
- /// Gets a fallback font file.
- /// </summary>
- /// <param name="name">The name of the fallback font file to get.</param>
- /// <response code="200">Fallback font file retrieved.</response>
- /// <returns>The fallback font file.</returns>
- [HttpGet("FallbackFont/Fonts/{name}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile("font/*")]
- public ActionResult GetFallbackFont([FromRoute, Required] string name)
+ /// <summary>
+ /// Gets a fallback font file.
+ /// </summary>
+ /// <param name="name">The name of the fallback font file to get.</param>
+ /// <response code="200">Fallback font file retrieved.</response>
+ /// <returns>The fallback font file.</returns>
+ [HttpGet("FallbackFont/Fonts/{name}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile("font/*")]
+ public ActionResult GetFallbackFont([FromRoute, Required] string name)
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var fallbackFontPath = encodingOptions.FallbackFontPath;
+
+ if (!string.IsNullOrEmpty(fallbackFontPath))
{
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var fallbackFontPath = encodingOptions.FallbackFontPath;
+ var fontFile = _fileSystem.GetFiles(fallbackFontPath)
+ .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ var fileSize = fontFile?.Length;
- if (!string.IsNullOrEmpty(fallbackFontPath))
+ if (fontFile is not null && fileSize is not null && fileSize > 0)
{
- var fontFile = _fileSystem.GetFiles(fallbackFontPath)
- .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
- var fileSize = fontFile?.Length;
-
- if (fontFile is not null && fileSize is not null && fileSize > 0)
- {
- _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize);
- return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
- }
- else
- {
- _logger.LogWarning("The selected font is null or empty");
- }
+ _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize);
+ return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
}
else
{
- _logger.LogWarning("The path of fallback font folder has not been set");
- encodingOptions.EnableFallbackFont = false;
+ _logger.LogWarning("The selected font is null or empty");
}
-
- // returning HTTP 204 will break the SubtitlesOctopus
- return Ok();
}
+ else
+ {
+ _logger.LogWarning("The path of fallback font folder has not been set");
+ encodingOptions.EnableFallbackFont = false;
+ }
+
+ // returning HTTP 204 will break the SubtitlesOctopus
+ return Ok();
}
}
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 1cf528153..5b808f257 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -13,80 +12,79 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The suggestions controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class SuggestionsController : BaseJellyfinApiController
{
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+
/// <summary>
- /// The suggestions controller.
+ /// Initializes a new instance of the <see cref="SuggestionsController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class SuggestionsController : BaseJellyfinApiController
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public SuggestionsController(
+ IDtoService dtoService,
+ IUserManager userManager,
+ ILibraryManager libraryManager)
{
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
+ _dtoService = dtoService;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="SuggestionsController"/> class.
- /// </summary>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public SuggestionsController(
- IDtoService dtoService,
- IUserManager userManager,
- ILibraryManager libraryManager)
- {
- _dtoService = dtoService;
- _userManager = userManager;
- _libraryManager = libraryManager;
- }
+ /// <summary>
+ /// Gets suggestions.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="mediaType">The media types.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="startIndex">Optional. The start index.</param>
+ /// <param name="limit">Optional. The limit.</param>
+ /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
+ /// <response code="200">Suggestions returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
+ [HttpGet("Users/{userId}/Suggestions")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
+ [FromRoute, Required] Guid userId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool enableTotalRecordCount = false)
+ {
+ var user = userId.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId);
- /// <summary>
- /// Gets suggestions.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="mediaType">The media types.</param>
- /// <param name="type">The type.</param>
- /// <param name="startIndex">Optional. The start index.</param>
- /// <param name="limit">Optional. The limit.</param>
- /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
- /// <response code="200">Suggestions returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
- [HttpGet("Users/{userId}/Suggestions")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
- [FromRoute, Required] Guid userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool enableTotalRecordCount = false)
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
- var user = userId.Equals(default)
- ? null
- : _userManager.GetUserById(userId);
-
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
- {
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
- MediaTypes = mediaType,
- IncludeItemTypes = type,
- IsVirtualItem = false,
- StartIndex = startIndex,
- Limit = limit,
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = enableTotalRecordCount,
- Recursive = true
- });
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
+ MediaTypes = mediaType,
+ IncludeItemTypes = type,
+ IsVirtualItem = false,
+ StartIndex = startIndex,
+ Limit = limit,
+ DtoOptions = dtoOptions,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ Recursive = true
+ });
- var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
+ var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
- return new QueryResult<BaseItemDto>(
- startIndex,
- result.TotalRecordCount,
- dtoList);
- }
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ result.TotalRecordCount,
+ dtoList);
}
}
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 99347246e..23abba7dc 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -16,409 +16,408 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The sync play controller.
+/// </summary>
+[Authorize(Policy = Policies.SyncPlayHasAccess)]
+public class SyncPlayController : BaseJellyfinApiController
{
+ private readonly ISessionManager _sessionManager;
+ private readonly ISyncPlayManager _syncPlayManager;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// The sync play controller.
+ /// Initializes a new instance of the <see cref="SyncPlayController"/> class.
/// </summary>
- [Authorize(Policy = Policies.SyncPlayHasAccess)]
- public class SyncPlayController : BaseJellyfinApiController
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public SyncPlayController(
+ ISessionManager sessionManager,
+ ISyncPlayManager syncPlayManager,
+ IUserManager userManager)
{
- private readonly ISessionManager _sessionManager;
- private readonly ISyncPlayManager _syncPlayManager;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SyncPlayController"/> class.
- /// </summary>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public SyncPlayController(
- ISessionManager sessionManager,
- ISyncPlayManager syncPlayManager,
- IUserManager userManager)
- {
- _sessionManager = sessionManager;
- _syncPlayManager = syncPlayManager;
- _userManager = userManager;
- }
+ _sessionManager = sessionManager;
+ _syncPlayManager = syncPlayManager;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Create a new SyncPlay group.
- /// </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>
- [HttpPost("New")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayCreateGroup)]
- public async Task<ActionResult> 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();
- }
+ /// <summary>
+ /// Create a new SyncPlay group.
+ /// </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>
+ [HttpPost("New")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayCreateGroup)]
+ public async Task<ActionResult> 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();
+ }
- /// <summary>
- /// Join an existing SyncPlay group.
- /// </summary>
- /// <param name="requestData">The group to join.</param>
- /// <response code="204">Group join successful.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Join")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayJoinGroup)]
- public async Task<ActionResult> SyncPlayJoinGroup(
- [FromBody, Required] JoinGroupRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
- _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Join an existing SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The group to join.</param>
+ /// <response code="204">Group join successful.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Join")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayJoinGroup)]
+ public async Task<ActionResult> SyncPlayJoinGroup(
+ [FromBody, Required] JoinGroupRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
+ _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Leave the joined SyncPlay group.
- /// </summary>
- /// <response code="204">Group leave successful.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Leave")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayLeaveGroup()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new LeaveGroupRequest();
- _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Leave the joined SyncPlay group.
+ /// </summary>
+ /// <response code="204">Group leave successful.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Leave")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayLeaveGroup()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new LeaveGroupRequest();
+ _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Gets all SyncPlay groups.
- /// </summary>
- /// <response code="200">Groups returned.</response>
- /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
- [HttpGet("List")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.SyncPlayJoinGroup)]
- public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new ListGroupsRequest();
- return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable());
- }
+ /// <summary>
+ /// Gets all SyncPlay groups.
+ /// </summary>
+ /// <response code="200">Groups returned.</response>
+ /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
+ [HttpGet("List")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.SyncPlayJoinGroup)]
+ public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new ListGroupsRequest();
+ return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable());
+ }
- /// <summary>
- /// Request to set new playlist in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new playlist to play in the group.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetNewQueue")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetNewQueue(
- [FromBody, Required] PlayRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PlayGroupRequest(
- requestData.PlayingQueue,
- requestData.PlayingItemPosition,
- requestData.StartPositionTicks);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to set new playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new playlist to play in the group.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetNewQueue")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetNewQueue(
+ [FromBody, Required] PlayRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PlayGroupRequest(
+ requestData.PlayingQueue,
+ requestData.PlayingItemPosition,
+ requestData.StartPositionTicks);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to change playlist item in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new item to play.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetPlaylistItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetPlaylistItem(
- [FromBody, Required] SetPlaylistItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to change playlist item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new item to play.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetPlaylistItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetPlaylistItem(
+ [FromBody, Required] SetPlaylistItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to remove items from the playlist in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The items to remove.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("RemoveFromPlaylist")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
- [FromBody, Required] RemoveFromPlaylistRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to remove items from the playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The items to remove.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("RemoveFromPlaylist")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
+ [FromBody, Required] RemoveFromPlaylistRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to move an item in the playlist in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new position for the item.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("MovePlaylistItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayMovePlaylistItem(
- [FromBody, Required] MovePlaylistItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to move an item in the playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new position for the item.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("MovePlaylistItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayMovePlaylistItem(
+ [FromBody, Required] MovePlaylistItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to queue items to the playlist of a SyncPlay group.
- /// </summary>
- /// <param name="requestData">The items to add.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Queue")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayQueue(
- [FromBody, Required] QueueRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to queue items to the playlist of a SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The items to add.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Queue")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayQueue(
+ [FromBody, Required] QueueRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request unpause in SyncPlay group.
- /// </summary>
- /// <response code="204">Unpause update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Unpause")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayUnpause()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new UnpauseGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request unpause in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Unpause update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Unpause")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayUnpause()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new UnpauseGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request pause in SyncPlay group.
- /// </summary>
- /// <response code="204">Pause update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Pause")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayPause()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PauseGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request pause in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Pause update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Pause")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayPause()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PauseGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request stop in SyncPlay group.
- /// </summary>
- /// <response code="204">Stop update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Stop")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayStop()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new StopGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request stop in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Stop update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Stop")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayStop()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new StopGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request seek in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new playback position.</param>
- /// <response code="204">Seek update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Seek")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySeek(
- [FromBody, Required] SeekRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request seek in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new playback position.</param>
+ /// <response code="204">Seek update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Seek")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySeek(
+ [FromBody, Required] SeekRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Notify SyncPlay group that member is buffering.
- /// </summary>
- /// <param name="requestData">The player status.</param>
- /// <response code="204">Group state update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Buffering")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayBuffering(
- [FromBody, Required] BufferRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new BufferGroupRequest(
- requestData.When,
- requestData.PositionTicks,
- requestData.IsPlaying,
- requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Notify SyncPlay group that member is buffering.
+ /// </summary>
+ /// <param name="requestData">The player status.</param>
+ /// <response code="204">Group state update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Buffering")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayBuffering(
+ [FromBody, Required] BufferRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new BufferGroupRequest(
+ requestData.When,
+ requestData.PositionTicks,
+ requestData.IsPlaying,
+ requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Notify SyncPlay group that member is ready for playback.
- /// </summary>
- /// <param name="requestData">The player status.</param>
- /// <response code="204">Group state update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Ready")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayReady(
- [FromBody, Required] ReadyRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new ReadyGroupRequest(
- requestData.When,
- requestData.PositionTicks,
- requestData.IsPlaying,
- requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Notify SyncPlay group that member is ready for playback.
+ /// </summary>
+ /// <param name="requestData">The player status.</param>
+ /// <response code="204">Group state update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Ready")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayReady(
+ [FromBody, Required] ReadyRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new ReadyGroupRequest(
+ requestData.When,
+ requestData.PositionTicks,
+ requestData.IsPlaying,
+ requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request SyncPlay group to ignore member during group-wait.
- /// </summary>
- /// <param name="requestData">The settings to set.</param>
- /// <response code="204">Member state updated.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetIgnoreWait")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetIgnoreWait(
- [FromBody, Required] IgnoreWaitRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request SyncPlay group to ignore member during group-wait.
+ /// </summary>
+ /// <param name="requestData">The settings to set.</param>
+ /// <response code="204">Member state updated.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetIgnoreWait")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetIgnoreWait(
+ [FromBody, Required] IgnoreWaitRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request next item in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The current item information.</param>
- /// <response code="204">Next item update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("NextItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayNextItem(
- [FromBody, Required] NextItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request next item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The current item information.</param>
+ /// <response code="204">Next item update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("NextItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayNextItem(
+ [FromBody, Required] NextItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request previous item in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The current item information.</param>
- /// <response code="204">Previous item update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("PreviousItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayPreviousItem(
- [FromBody, Required] PreviousItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request previous item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The current item information.</param>
+ /// <response code="204">Previous item update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("PreviousItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayPreviousItem(
+ [FromBody, Required] PreviousItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to set repeat mode in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new repeat mode.</param>
- /// <response code="204">Play queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetRepeatMode")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetRepeatMode(
- [FromBody, Required] SetRepeatModeRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to set repeat mode in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new repeat mode.</param>
+ /// <response code="204">Play queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetRepeatMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetRepeatMode(
+ [FromBody, Required] SetRepeatModeRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to set shuffle mode in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new shuffle mode.</param>
- /// <response code="204">Play queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetShuffleMode")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetShuffleMode(
- [FromBody, Required] SetShuffleModeRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to set shuffle mode in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new shuffle mode.</param>
+ /// <response code="204">Play queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetShuffleMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetShuffleMode(
+ [FromBody, Required] SetShuffleModeRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Update session ping.
- /// </summary>
- /// <param name="requestData">The new ping.</param>
- /// <response code="204">Ping updated.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Ping")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> SyncPlayPing(
- [FromBody, Required] PingRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PingGroupRequest(requestData.Ping);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Update session ping.
+ /// </summary>
+ /// <param name="requestData">The new ping.</param>
+ /// <response code="204">Ping updated.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Ping")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> SyncPlayPing(
+ [FromBody, Required] PingRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PingGroupRequest(requestData.Ping);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 2d594293e..4ab705f40 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -20,204 +20,203 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The system controller.
+/// </summary>
+public class SystemController : BaseJellyfinApiController
{
+ private readonly IServerApplicationHost _appHost;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IFileSystem _fileSystem;
+ private readonly INetworkManager _network;
+ private readonly ILogger<SystemController> _logger;
+
/// <summary>
- /// The system controller.
+ /// Initializes a new instance of the <see cref="SystemController"/> class.
/// </summary>
- public class SystemController : BaseJellyfinApiController
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+ /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+ public SystemController(
+ IServerConfigurationManager serverConfigurationManager,
+ IServerApplicationHost appHost,
+ IFileSystem fileSystem,
+ INetworkManager network,
+ ILogger<SystemController> logger)
{
- private readonly IServerApplicationHost _appHost;
- private readonly IApplicationPaths _appPaths;
- private readonly IFileSystem _fileSystem;
- private readonly INetworkManager _network;
- private readonly ILogger<SystemController> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SystemController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
- /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
- /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
- /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
- public SystemController(
- IServerConfigurationManager serverConfigurationManager,
- IServerApplicationHost appHost,
- IFileSystem fileSystem,
- INetworkManager network,
- ILogger<SystemController> logger)
- {
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _appHost = appHost;
- _fileSystem = fileSystem;
- _network = network;
- _logger = logger;
- }
+ _appPaths = serverConfigurationManager.ApplicationPaths;
+ _appHost = appHost;
+ _fileSystem = fileSystem;
+ _network = network;
+ _logger = logger;
+ }
- /// <summary>
- /// Gets information about the server.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
- [HttpGet("Info")]
- [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<SystemInfo> GetSystemInfo()
- {
- return _appHost.GetSystemInfo(Request);
- }
+ /// <summary>
+ /// Gets information about the server.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
+ [HttpGet("Info")]
+ [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<SystemInfo> GetSystemInfo()
+ {
+ return _appHost.GetSystemInfo(Request);
+ }
- /// <summary>
- /// Gets public information about the server.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
- [HttpGet("Info/Public")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
- {
- return _appHost.GetPublicSystemInfo(Request);
- }
+ /// <summary>
+ /// Gets public information about the server.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
+ [HttpGet("Info/Public")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
+ {
+ return _appHost.GetPublicSystemInfo(Request);
+ }
- /// <summary>
- /// Pings the system.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>The server name.</returns>
- [HttpGet("Ping", Name = "GetPingSystem")]
- [HttpPost("Ping", Name = "PostPingSystem")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<string> PingSystem()
- {
- return _appHost.Name;
- }
+ /// <summary>
+ /// Pings the system.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>The server name.</returns>
+ [HttpGet("Ping", Name = "GetPingSystem")]
+ [HttpPost("Ping", Name = "PostPingSystem")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<string> PingSystem()
+ {
+ return _appHost.Name;
+ }
- /// <summary>
- /// Restarts the application.
- /// </summary>
- /// <response code="204">Server restarted.</response>
- /// <returns>No content. Server restarted.</returns>
- [HttpPost("Restart")]
- [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult RestartApplication()
+ /// <summary>
+ /// Restarts the application.
+ /// </summary>
+ /// <response code="204">Server restarted.</response>
+ /// <returns>No content. Server restarted.</returns>
+ [HttpPost("Restart")]
+ [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult RestartApplication()
+ {
+ Task.Run(async () =>
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- _appHost.Restart();
- });
- return NoContent();
- }
+ await Task.Delay(100).ConfigureAwait(false);
+ _appHost.Restart();
+ });
+ return NoContent();
+ }
- /// <summary>
- /// Shuts down the application.
- /// </summary>
- /// <response code="204">Server shut down.</response>
- /// <returns>No content. Server shut down.</returns>
- [HttpPost("Shutdown")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult ShutdownApplication()
+ /// <summary>
+ /// Shuts down the application.
+ /// </summary>
+ /// <response code="204">Server shut down.</response>
+ /// <returns>No content. Server shut down.</returns>
+ [HttpPost("Shutdown")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult ShutdownApplication()
+ {
+ Task.Run(async () =>
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- await _appHost.Shutdown().ConfigureAwait(false);
- });
- return NoContent();
- }
+ await Task.Delay(100).ConfigureAwait(false);
+ await _appHost.Shutdown().ConfigureAwait(false);
+ });
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets a list of available server log files.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns>
+ [HttpGet("Logs")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<LogFile[]> GetServerLogs()
+ {
+ IEnumerable<FileSystemMetadata> files;
- /// <summary>
- /// Gets a list of available server log files.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns>
- [HttpGet("Logs")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<LogFile[]> GetServerLogs()
+ try
{
- IEnumerable<FileSystemMetadata> files;
-
- try
- {
- files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error getting logs");
- files = Enumerable.Empty<FileSystemMetadata>();
- }
-
- var result = files.Select(i => new LogFile
- {
- DateCreated = _fileSystem.GetCreationTimeUtc(i),
- DateModified = _fileSystem.GetLastWriteTimeUtc(i),
- Name = i.Name,
- Size = i.Length
- })
- .OrderByDescending(i => i.DateModified)
- .ThenByDescending(i => i.DateCreated)
- .ThenBy(i => i.Name)
- .ToArray();
-
- return result;
+ files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false);
}
-
- /// <summary>
- /// Gets information about the request endpoint.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns>
- [HttpGet("Endpoint")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<EndPointInfo> GetEndpointInfo()
+ catch (IOException ex)
{
- return new EndPointInfo
- {
- IsLocal = HttpContext.IsLocal(),
- IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())
- };
+ _logger.LogError(ex, "Error getting logs");
+ files = Enumerable.Empty<FileSystemMetadata>();
}
- /// <summary>
- /// Gets a log file.
- /// </summary>
- /// <param name="name">The name of the log file to get.</param>
- /// <response code="200">Log file retrieved.</response>
- /// <returns>The log file.</returns>
- [HttpGet("Logs/Log")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile(MediaTypeNames.Text.Plain)]
- public ActionResult GetLogFile([FromQuery, Required] string name)
+ var result = files.Select(i => new LogFile
{
- var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
- .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ DateCreated = _fileSystem.GetCreationTimeUtc(i),
+ DateModified = _fileSystem.GetLastWriteTimeUtc(i),
+ Name = i.Name,
+ Size = i.Length
+ })
+ .OrderByDescending(i => i.DateModified)
+ .ThenByDescending(i => i.DateCreated)
+ .ThenBy(i => i.Name)
+ .ToArray();
- // 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, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- return File(stream, "text/plain; charset=utf-8");
- }
+ return result;
+ }
- /// <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(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
+ /// <summary>
+ /// Gets information about the request endpoint.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns>
+ [HttpGet("Endpoint")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<EndPointInfo> GetEndpointInfo()
+ {
+ return new EndPointInfo
{
- var result = _network.GetMacAddresses()
- .Select(i => new WakeOnLanInfo(i));
- return Ok(result);
- }
+ IsLocal = HttpContext.IsLocal(),
+ IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())
+ };
+ }
+
+ /// <summary>
+ /// Gets a log file.
+ /// </summary>
+ /// <param name="name">The name of the log file to get.</param>
+ /// <response code="200">Log file retrieved.</response>
+ /// <returns>The log file.</returns>
+ [HttpGet("Logs/Log")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Text.Plain)]
+ public ActionResult GetLogFile([FromQuery, Required] string name)
+ {
+ var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
+ .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+
+ // For older files, assume fully static
+ var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
+ FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, 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 = _network.GetMacAddresses()
+ .Select(i => new WakeOnLanInfo(i));
+ return Ok(result);
}
}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index e7c5a7125..d7304cf42 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -3,32 +3,31 @@ using MediaBrowser.Model.SyncPlay;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The time sync controller.
+/// </summary>
+[Route("")]
+public class TimeSyncController : BaseJellyfinApiController
{
/// <summary>
- /// The time sync controller.
+ /// Gets the current UTC time.
/// </summary>
- [Route("")]
- public class TimeSyncController : BaseJellyfinApiController
+ /// <response code="200">Time returned.</response>
+ /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
+ [HttpGet("GetUtcTime")]
+ [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
+ public ActionResult<UtcTimeResponse> GetUtcTime()
{
- /// <summary>
- /// Gets the current UTC time.
- /// </summary>
- /// <response code="200">Time returned.</response>
- /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
- [HttpGet("GetUtcTime")]
- [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
- public ActionResult<UtcTimeResponse> GetUtcTime()
- {
- // Important to keep the following line at the beginning
- var requestReceptionTime = DateTime.UtcNow;
+ // Important to keep the following line at the beginning
+ var requestReceptionTime = DateTime.UtcNow;
- // Important to keep the following line at the end
- var responseTransmissionTime = DateTime.UtcNow;
+ // Important to keep the following line at the end
+ var responseTransmissionTime = DateTime.UtcNow;
- // Implementing NTP on such a high level results in this useless
- // information being sent. On the other hand it enables future additions.
- return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
- }
+ // Implementing NTP on such a high level results in this useless
+ // information being sent. On the other hand it enables future additions.
+ return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
}
}
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 53a839e43..b5b640620 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,6 +1,4 @@
using System;
-using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
@@ -10,290 +8,289 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The trailers controller.
+/// </summary>
+[Authorize]
+public class TrailersController : BaseJellyfinApiController
{
+ private readonly ItemsController _itemsController;
+
/// <summary>
- /// The trailers controller.
+ /// Initializes a new instance of the <see cref="TrailersController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class TrailersController : BaseJellyfinApiController
+ /// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
+ public TrailersController(ItemsController itemsController)
{
- private readonly ItemsController _itemsController;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TrailersController"/> class.
- /// </summary>
- /// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
- public TrailersController(ItemsController itemsController)
- {
- _itemsController = itemsController;
- }
+ _itemsController = itemsController;
+ }
- /// <summary>
- /// Finds movies and trailers similar to a given trailer.
- /// </summary>
- /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
- /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
- /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
- /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
- /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
- /// <param name="hasTrailer">Optional filter by items with trailers.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
- /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
- /// <param name="isHd">Optional filter by items that are HD or not.</param>
- /// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
- /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
- /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
- /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
- /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
- /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
- /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
- /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
- /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
- /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
- /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
- /// <param name="isMovie">Optional filter for live tv movies.</param>
- /// <param name="isSeries">Optional filter for live tv series.</param>
- /// <param name="isNews">Optional filter for live tv news.</param>
- /// <param name="isKids">Optional filter for live tv kids.</param>
- /// <param name="isSports">Optional filter for live tv sports.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
- /// <param name="searchTerm">Optional. Filter based on a search term.</param>
- /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
- /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
- /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
- /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
- /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
- /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="isLocked">Optional filter by items that are locked.</param>
- /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
- /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
- /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
- /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
- /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
- /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
- /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
- /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
- /// <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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
- [FromQuery] Guid? userId,
- [FromQuery] string? maxOfficialRating,
- [FromQuery] bool? hasThemeSong,
- [FromQuery] bool? hasThemeVideo,
- [FromQuery] bool? hasSubtitles,
- [FromQuery] bool? hasSpecialFeature,
- [FromQuery] bool? hasTrailer,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] int? parentIndexNumber,
- [FromQuery] bool? hasParentalRating,
- [FromQuery] bool? isHd,
- [FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
- [FromQuery] bool? isMissing,
- [FromQuery] bool? isUnaired,
- [FromQuery] double? minCommunityRating,
- [FromQuery] double? minCriticRating,
- [FromQuery] DateTime? minPremiereDate,
- [FromQuery] DateTime? minDateLastSaved,
- [FromQuery] DateTime? minDateLastSavedForUser,
- [FromQuery] DateTime? maxPremiereDate,
- [FromQuery] bool? hasOverview,
- [FromQuery] bool? hasImdbId,
- [FromQuery] bool? hasTmdbId,
- [FromQuery] bool? hasTvdbId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? recursive,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
- [FromQuery] string? minOfficialRating,
- [FromQuery] bool? isLocked,
- [FromQuery] bool? isPlaceHolder,
- [FromQuery] bool? hasOfficialRating,
- [FromQuery] bool? collapseBoxSetItems,
- [FromQuery] int? minWidth,
- [FromQuery] int? minHeight,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true)
- {
- var includeItemTypes = new[] { BaseItemKind.Trailer };
+ /// <summary>
+ /// Finds movies and trailers similar to a given trailer.
+ /// </summary>
+ /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
+ /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+ /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+ /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+ /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+ /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+ /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+ /// <param name="isHd">Optional filter by items that are HD or not.</param>
+ /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+ /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+ /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+ /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+ /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+ /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+ /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+ /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+ /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+ /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
+ /// <param name="isMovie">Optional filter for live tv movies.</param>
+ /// <param name="isSeries">Optional filter for live tv series.</param>
+ /// <param name="isNews">Optional filter for live tv news.</param>
+ /// <param name="isKids">Optional filter for live tv kids.</param>
+ /// <param name="isSports">Optional filter for live tv sports.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+ /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+ /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
+ /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+ /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+ /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
+ /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+ /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+ /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
+ /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
+ /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="isLocked">Optional filter by items that are locked.</param>
+ /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+ /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+ /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+ /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+ /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+ /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+ /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+ /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
+ /// <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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
+ [FromQuery] Guid? userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery] bool? isMissing,
+ [FromQuery] bool? isUnaired,
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] double? minCriticRating,
+ [FromQuery] DateTime? minPremiereDate,
+ [FromQuery] DateTime? minDateLastSaved,
+ [FromQuery] DateTime? minDateLastSavedForUser,
+ [FromQuery] DateTime? maxPremiereDate,
+ [FromQuery] bool? hasOverview,
+ [FromQuery] bool? hasImdbId,
+ [FromQuery] bool? hasTmdbId,
+ [FromQuery] bool? hasTvdbId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery] string? minOfficialRating,
+ [FromQuery] bool? isLocked,
+ [FromQuery] bool? isPlaceHolder,
+ [FromQuery] bool? hasOfficialRating,
+ [FromQuery] bool? collapseBoxSetItems,
+ [FromQuery] int? minWidth,
+ [FromQuery] int? minHeight,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? is3D,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ var includeItemTypes = new[] { BaseItemKind.Trailer };
- return _itemsController
- .GetItems(
- userId,
- maxOfficialRating,
- hasThemeSong,
- hasThemeVideo,
- hasSubtitles,
- hasSpecialFeature,
- hasTrailer,
- adjacentTo,
- parentIndexNumber,
- hasParentalRating,
- isHd,
- is4K,
- locationTypes,
- excludeLocationTypes,
- isMissing,
- isUnaired,
- minCommunityRating,
- minCriticRating,
- minPremiereDate,
- minDateLastSaved,
- minDateLastSavedForUser,
- maxPremiereDate,
- hasOverview,
- hasImdbId,
- hasTmdbId,
- hasTvdbId,
- isMovie,
- isSeries,
- isNews,
- isKids,
- isSports,
- excludeItemIds,
- startIndex,
- limit,
- recursive,
- searchTerm,
- sortOrder,
- parentId,
- fields,
- excludeItemTypes,
- includeItemTypes,
- filters,
- isFavorite,
- mediaTypes,
- imageTypes,
- sortBy,
- isPlayed,
- genres,
- officialRatings,
- tags,
- years,
- enableUserData,
- imageTypeLimit,
- enableImageTypes,
- person,
- personIds,
- personTypes,
- studios,
- artists,
- excludeArtistIds,
- artistIds,
- albumArtistIds,
- contributingArtistIds,
- albums,
- albumIds,
- ids,
- videoTypes,
- minOfficialRating,
- isLocked,
- isPlaceHolder,
- hasOfficialRating,
- collapseBoxSetItems,
- minWidth,
- minHeight,
- maxWidth,
- maxHeight,
- is3D,
- seriesStatus,
- nameStartsWithOrGreater,
- nameStartsWith,
- nameLessThan,
- studioIds,
- genreIds,
- enableTotalRecordCount,
- enableImages);
- }
+ return _itemsController
+ .GetItems(
+ userId,
+ maxOfficialRating,
+ hasThemeSong,
+ hasThemeVideo,
+ hasSubtitles,
+ hasSpecialFeature,
+ hasTrailer,
+ adjacentTo,
+ parentIndexNumber,
+ hasParentalRating,
+ isHd,
+ is4K,
+ locationTypes,
+ excludeLocationTypes,
+ isMissing,
+ isUnaired,
+ minCommunityRating,
+ minCriticRating,
+ minPremiereDate,
+ minDateLastSaved,
+ minDateLastSavedForUser,
+ maxPremiereDate,
+ hasOverview,
+ hasImdbId,
+ hasTmdbId,
+ hasTvdbId,
+ isMovie,
+ isSeries,
+ isNews,
+ isKids,
+ isSports,
+ excludeItemIds,
+ startIndex,
+ limit,
+ recursive,
+ searchTerm,
+ sortOrder,
+ parentId,
+ fields,
+ excludeItemTypes,
+ includeItemTypes,
+ filters,
+ isFavorite,
+ mediaTypes,
+ imageTypes,
+ sortBy,
+ isPlayed,
+ genres,
+ officialRatings,
+ tags,
+ years,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes,
+ person,
+ personIds,
+ personTypes,
+ studios,
+ artists,
+ excludeArtistIds,
+ artistIds,
+ albumArtistIds,
+ contributingArtistIds,
+ albums,
+ albumIds,
+ ids,
+ videoTypes,
+ minOfficialRating,
+ isLocked,
+ isPlaceHolder,
+ hasOfficialRating,
+ collapseBoxSetItems,
+ minWidth,
+ minHeight,
+ maxWidth,
+ maxHeight,
+ is3D,
+ seriesStatus,
+ nameStartsWithOrGreater,
+ nameStartsWith,
+ nameLessThan,
+ studioIds,
+ genreIds,
+ enableTotalRecordCount,
+ enableImages);
}
}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 7f4f4d077..7d23281f2 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -2,8 +2,8 @@ using System;
using System.Collections.Generic;
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 Jellyfin.Extensions;
@@ -19,366 +19,369 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The tv shows controller.
+/// </summary>
+[Route("Shows")]
+[Authorize]
+public class TvShowsController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly ITVSeriesManager _tvSeriesManager;
+
/// <summary>
- /// The tv shows controller.
+ /// Initializes a new instance of the <see cref="TvShowsController"/> class.
/// </summary>
- [Route("Shows")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class TvShowsController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param>
+ public TvShowsController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ ITVSeriesManager tvSeriesManager)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly ITVSeriesManager _tvSeriesManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TvShowsController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param>
- public TvShowsController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- ITVSeriesManager tvSeriesManager)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _tvSeriesManager = tvSeriesManager;
- }
-
- /// <summary>
- /// Gets a list of next up episodes.
- /// </summary>
- /// <param name="userId">The user id of the user to get the next up episodes for.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="seriesId">Optional. Filter by series id.</param>
- /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <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>
- /// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
- [HttpGet("NextUp")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] Guid? seriesId,
- [FromQuery] Guid? parentId,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] DateTime? nextUpDateCutoff,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool disableFirstEpisode = false,
- [FromQuery] bool enableRewatching = false)
- {
- var options = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- var result = _tvSeriesManager.GetNextUp(
- new NextUpQuery
- {
- Limit = limit,
- ParentId = parentId,
- SeriesId = seriesId,
- StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
- EnableTotalRecordCount = enableTotalRecordCount,
- DisableFirstEpisode = disableFirstEpisode,
- NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
- EnableRewatching = enableRewatching
- },
- options);
-
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
-
- return new QueryResult<BaseItemDto>(
- startIndex,
- result.TotalRecordCount,
- returnItems);
- }
-
- /// <summary>
- /// Gets a list of upcoming episodes.
- /// </summary>
- /// <param name="userId">The user id of the user to get the upcoming episodes for.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
- [HttpGet("Upcoming")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] Guid? parentId,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
-
- var parentIdGuid = parentId ?? Guid.Empty;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _tvSeriesManager = tvSeriesManager;
+ }
- var options = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ /// <summary>
+ /// Gets a list of next up episodes.
+ /// </summary>
+ /// <param name="userId">The user id of the user to get the next up episodes for.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="seriesId">Optional. Filter by series id.</param>
+ /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <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>
+ /// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+ [HttpGet("NextUp")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] Guid? seriesId,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] DateTime? nextUpDateCutoff,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool disableFirstEpisode = false,
+ [FromQuery] bool enableRewatching = false)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var options = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var result = _tvSeriesManager.GetNextUp(
+ new NextUpQuery
{
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
- MinPremiereDate = minPremiereDate,
- StartIndex = startIndex,
Limit = limit,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = options
- });
+ ParentId = parentId,
+ SeriesId = seriesId,
+ StartIndex = startIndex,
+ UserId = userId.Value,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ DisableFirstEpisode = disableFirstEpisode,
+ NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
+ EnableRewatching = enableRewatching
+ },
+ options);
+
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
+
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ result.TotalRecordCount,
+ returnItems);
+ }
- var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
+ /// <summary>
+ /// Gets a list of upcoming episodes.
+ /// </summary>
+ /// <param name="userId">The user id of the user to get the upcoming episodes for.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+ [HttpGet("Upcoming")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- return new QueryResult<BaseItemDto>(
- startIndex,
- itemsResult.Count,
- returnItems);
- }
+ var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
- /// <summary>
- /// Gets episodes for a tv season.
- /// </summary>
- /// <param name="seriesId">The series id.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
- /// <param name="season">Optional filter by season number.</param>
- /// <param name="seasonId">Optional. Filter by season id.</param>
- /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="startItemId">Optional. Skip through the list until a given item is found.</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="enableImages">Optional, include image information in output.</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>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
- [HttpGet("{seriesId}/Episodes")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
- [FromRoute, Required] Guid seriesId,
- [FromQuery] Guid? userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] int? season,
- [FromQuery] Guid? seasonId,
- [FromQuery] bool? isMissing,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] Guid? startItemId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] string? sortBy)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ var parentIdGuid = parentId ?? Guid.Empty;
- List<BaseItem> episodes;
+ var options = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
+ OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
+ MinPremiereDate = minPremiereDate,
+ StartIndex = startIndex,
+ Limit = limit,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ DtoOptions = options
+ });
+
+ var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
+
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ itemsResult.Count,
+ returnItems);
+ }
- if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
- {
- var item = _libraryManager.GetItemById(seasonId.Value);
- if (item is not Season seasonItem)
- {
- return NotFound("No season exists with Id " + seasonId);
- }
+ /// <summary>
+ /// Gets episodes for a tv season.
+ /// </summary>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="season">Optional filter by season number.</param>
+ /// <param name="seasonId">Optional. Filter by season id.</param>
+ /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="startItemId">Optional. Skip through the list until a given item is found.</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="enableImages">Optional, include image information in output.</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>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
+ [HttpGet("{seriesId}/Episodes")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
+ [FromRoute, Required] Guid seriesId,
+ [FromQuery] Guid? userId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] int? season,
+ [FromQuery] Guid? seasonId,
+ [FromQuery] bool? isMissing,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] Guid? startItemId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] string? sortBy)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- episodes = seasonItem.GetEpisodes(user, dtoOptions);
- }
- else if (season.HasValue) // Season number was supplied. Get episodes by season number
- {
- if (_libraryManager.GetItemById(seriesId) is not Series series)
- {
- return NotFound("Series not found");
- }
-
- var seasonItem = series
- .GetSeasons(user, dtoOptions)
- .FirstOrDefault(i => i.IndexNumber == season.Value);
-
- episodes = seasonItem is null ?
- new List<BaseItem>()
- : ((Season)seasonItem).GetEpisodes(user, dtoOptions);
- }
- else // No season number or season id was supplied. Returning all episodes.
- {
- if (_libraryManager.GetItemById(seriesId) is not Series series)
- {
- return NotFound("Series not found");
- }
+ List<BaseItem> episodes;
- episodes = series.GetEpisodes(user, dtoOptions).ToList();
- }
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- // Filter after the fact in case the ui doesn't want them
- if (isMissing.HasValue)
+ if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
+ {
+ var item = _libraryManager.GetItemById(seasonId.Value);
+ if (item is not Season seasonItem)
{
- var val = isMissing.Value;
- episodes = episodes
- .Where(i => ((Episode)i).IsMissingEpisode == val)
- .ToList();
+ return NotFound("No season exists with Id " + seasonId);
}
- if (startItemId.HasValue)
+ episodes = seasonItem.GetEpisodes(user, dtoOptions);
+ }
+ else if (season.HasValue) // Season number was supplied. Get episodes by season number
+ {
+ if (_libraryManager.GetItemById(seriesId) is not Series series)
{
- episodes = episodes
- .SkipWhile(i => !startItemId.Value.Equals(i.Id))
- .ToList();
+ return NotFound("Series not found");
}
- // This must be the last filter
- if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
- {
- episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
- }
+ var seasonItem = series
+ .GetSeasons(user, dtoOptions)
+ .FirstOrDefault(i => i.IndexNumber == season.Value);
- if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
+ episodes = seasonItem is null ?
+ new List<BaseItem>()
+ : ((Season)seasonItem).GetEpisodes(user, dtoOptions);
+ }
+ else // No season number or season id was supplied. Returning all episodes.
+ {
+ if (_libraryManager.GetItemById(seriesId) is not Series series)
{
- episodes.Shuffle();
+ return NotFound("Series not found");
}
- var returnItems = episodes;
+ episodes = series.GetEpisodes(user, dtoOptions).ToList();
+ }
- if (startIndex.HasValue || limit.HasValue)
- {
- returnItems = ApplyPaging(episodes, startIndex, limit).ToList();
- }
+ // Filter after the fact in case the ui doesn't want them
+ if (isMissing.HasValue)
+ {
+ var val = isMissing.Value;
+ episodes = episodes
+ .Where(i => ((Episode)i).IsMissingEpisode == val)
+ .ToList();
+ }
- var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
+ if (startItemId.HasValue)
+ {
+ episodes = episodes
+ .SkipWhile(i => !startItemId.Value.Equals(i.Id))
+ .ToList();
+ }
- return new QueryResult<BaseItemDto>(
- startIndex,
- episodes.Count,
- dtos);
+ // This must be the last filter
+ if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
+ {
+ episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
}
- /// <summary>
- /// Gets seasons for a tv series.
- /// </summary>
- /// <param name="seriesId">The series id.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
- /// <param name="isSpecialSeason">Optional. Filter by special season.</param>
- /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="enableImages">Optional. Include image information in output.</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>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
- [HttpGet("{seriesId}/Seasons")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
- [FromRoute, Required] Guid seriesId,
- [FromQuery] Guid? userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? isSpecialSeason,
- [FromQuery] bool? isMissing,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData)
+ if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ episodes.Shuffle();
+ }
- if (_libraryManager.GetItemById(seriesId) is not Series series)
- {
- return NotFound("Series not found");
- }
+ var returnItems = episodes;
- var seasons = series.GetItemList(new InternalItemsQuery(user)
- {
- IsMissing = isMissing,
- IsSpecialSeason = isSpecialSeason,
- AdjacentTo = adjacentTo
- });
+ if (startIndex.HasValue || limit.HasValue)
+ {
+ returnItems = ApplyPaging(episodes, startIndex, limit).ToList();
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
- var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ episodes.Count,
+ dtos);
+ }
- return new QueryResult<BaseItemDto>(returnItems);
+ /// <summary>
+ /// Gets seasons for a tv series.
+ /// </summary>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="isSpecialSeason">Optional. Filter by special season.</param>
+ /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
+ [HttpGet("{seriesId}/Seasons")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
+ [FromRoute, Required] Guid seriesId,
+ [FromQuery] Guid? userId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? isSpecialSeason,
+ [FromQuery] bool? isMissing,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ if (_libraryManager.GetItemById(seriesId) is not Series series)
+ {
+ return NotFound("Series not found");
}
- /// <summary>
- /// Applies the paging.
- /// </summary>
- /// <param name="items">The items.</param>
- /// <param name="startIndex">The start index.</param>
- /// <param name="limit">The limit.</param>
- /// <returns>IEnumerable{BaseItem}.</returns>
- private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> items, int? startIndex, int? limit)
+ var seasons = series.GetItemList(new InternalItemsQuery(user)
{
- // Start at
- if (startIndex.HasValue)
- {
- items = items.Skip(startIndex.Value);
- }
+ IsMissing = isMissing,
+ IsSpecialSeason = isSpecialSeason,
+ AdjacentTo = adjacentTo
+ });
- // Return limit
- if (limit.HasValue)
- {
- items = items.Take(limit.Value);
- }
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
- return items;
+ return new QueryResult<BaseItemDto>(returnItems);
+ }
+
+ /// <summary>
+ /// Applies the paging.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The limit.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> items, int? startIndex, int? limit)
+ {
+ // Start at
+ if (startIndex.HasValue)
+ {
+ items = items.Skip(startIndex.Value);
}
+
+ // Return limit
+ if (limit.HasValue)
+ {
+ items = items.Take(limit.Value);
+ }
+
+ return items;
}
}
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index d77126a35..12d033ae6 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -5,7 +5,6 @@ using System.Globalization;
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;
@@ -20,197 +19,160 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The universal audio controller.
+/// </summary>
+[Route("")]
+public class UniversalAudioController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<UniversalAudioController> _logger;
+ private readonly MediaInfoHelper _mediaInfoHelper;
+ private readonly AudioHelper _audioHelper;
+ private readonly DynamicHlsHelper _dynamicHlsHelper;
+
/// <summary>
- /// The universal audio controller.
+ /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
/// </summary>
- [Route("")]
- public class UniversalAudioController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
+ /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
+ /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+ /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
+ public UniversalAudioController(
+ ILibraryManager libraryManager,
+ ILogger<UniversalAudioController> logger,
+ MediaInfoHelper mediaInfoHelper,
+ AudioHelper audioHelper,
+ DynamicHlsHelper dynamicHlsHelper)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<UniversalAudioController> _logger;
- private readonly MediaInfoHelper _mediaInfoHelper;
- private readonly AudioHelper _audioHelper;
- private readonly DynamicHlsHelper _dynamicHlsHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
- /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
- /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
- /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
- public UniversalAudioController(
- ILibraryManager libraryManager,
- ILogger<UniversalAudioController> logger,
- MediaInfoHelper mediaInfoHelper,
- AudioHelper audioHelper,
- DynamicHlsHelper dynamicHlsHelper)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _mediaInfoHelper = mediaInfoHelper;
- _audioHelper = audioHelper;
- _dynamicHlsHelper = dynamicHlsHelper;
- }
-
- /// <summary>
- /// Gets an audio stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">Optional. The audio container.</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>
- /// <param name="userId">Optional. The user id.</param>
- /// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
- /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
- /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
- /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="transcodingContainer">Optional. The container to transcode to.</param>
- /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
- /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
- /// <response code="200">Audio stream returned.</response>
- /// <response code="302">Redirected to remote audio stream.</response>
- /// <returns>A <see cref="Task"/> containing the audio file.</returns>
- [HttpGet("Audio/{itemId}/universal")]
- [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status302Found)]
- [ProducesAudioFile]
- public async Task<ActionResult> GetUniversalAudioStream(
- [FromRoute, Required] Guid itemId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] Guid? userId,
- [FromQuery] string? audioCodec,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] int? transcodingAudioChannels,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] int? audioBitRate,
- [FromQuery] long? startTimeTicks,
- [FromQuery] string? transcodingContainer,
- [FromQuery] string? transcodingProtocol,
- [FromQuery] int? maxAudioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] bool? enableRemoteMedia,
- [FromQuery] bool breakOnNonKeyFrames = false,
- [FromQuery] bool enableRedirection = true)
- {
- var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _mediaInfoHelper = mediaInfoHelper;
+ _audioHelper = audioHelper;
+ _dynamicHlsHelper = dynamicHlsHelper;
+ }
- if (!userId.HasValue || userId.Value.Equals(default))
- {
- userId = User.GetUserId();
- }
+ /// <summary>
+ /// Gets an audio stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">Optional. The audio container.</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>
+ /// <param name="userId">Optional. The user id.</param>
+ /// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
+ /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
+ /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="transcodingContainer">Optional. The container to transcode to.</param>
+ /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
+ /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <response code="302">Redirected to remote audio stream.</response>
+ /// <returns>A <see cref="Task"/> containing the audio file.</returns>
+ [HttpGet("Audio/{itemId}/universal")]
+ [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status302Found)]
+ [ProducesAudioFile]
+ public async Task<ActionResult> GetUniversalAudioStream(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] int? transcodingAudioChannels,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] string? transcodingContainer,
+ [FromQuery] string? transcodingProtocol,
+ [FromQuery] int? maxAudioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] bool? enableRemoteMedia,
+ [FromQuery] bool breakOnNonKeyFrames = false,
+ [FromQuery] bool enableRedirection = true)
+ {
+ var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
+ userId = RequestHelpers.GetUserId(User, userId);
- _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
+ _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
- var info = await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId,
- mediaSourceId)
- .ConfigureAwait(false);
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId)
+ .ConfigureAwait(false);
- // set device specific data
- var item = _libraryManager.GetItemById(itemId);
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
- foreach (var sourceInfo in info.MediaSources)
- {
- _mediaInfoHelper.SetDeviceSpecificData(
- item,
- sourceInfo,
- deviceProfile,
- User,
- maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
- startTimeTicks ?? 0,
- mediaSourceId ?? string.Empty,
- null,
- null,
- maxAudioChannels,
- info.PlaySessionId!,
- userId ?? Guid.Empty,
- true,
- true,
- true,
- true,
- true,
- Request.HttpContext.GetNormalizedRemoteIp());
- }
-
- _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
-
- foreach (var source in info.MediaSources)
- {
- _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
- }
+ foreach (var sourceInfo in info.MediaSources)
+ {
+ _mediaInfoHelper.SetDeviceSpecificData(
+ item,
+ sourceInfo,
+ deviceProfile,
+ User,
+ maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+ startTimeTicks ?? 0,
+ mediaSourceId ?? string.Empty,
+ null,
+ null,
+ maxAudioChannels,
+ info.PlaySessionId!,
+ userId ?? Guid.Empty,
+ true,
+ true,
+ true,
+ true,
+ true,
+ Request.HttpContext.GetNormalizedRemoteIp());
+ }
- var mediaSource = info.MediaSources[0];
- if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
- {
- return Redirect(mediaSource.Path);
- }
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
- var isStatic = mediaSource.SupportsDirectStream;
- if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
- {
- // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
- // ffmpeg option -> file extension
- // mpegts -> ts
- // fmp4 -> mp4
- // TODO: remove this when we switch back to the segment muxer
- var supportedHlsContainers = new[] { "ts", "mp4" };
+ foreach (var source in info.MediaSources)
+ {
+ _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
+ }
- var dynamicHlsRequestDto = new HlsAudioRequestDto
- {
- Id = itemId,
- Container = ".m3u8",
- Static = isStatic,
- PlaySessionId = info.PlaySessionId,
- // fallback to mpegts if device reports some weird value unsupported by hls
- SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = true,
- AllowAudioStreamCopy = true,
- AllowVideoStreamCopy = true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames,
- AudioSampleRate = maxAudioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioBitRate = audioBitRate ?? maxStreamingBitrate,
- StartTimeTicks = startTimeTicks,
- SubtitleMethod = SubtitleDeliveryMethod.Hls,
- RequireAvc = false,
- DeInterlace = false,
- RequireNonAnamorphic = false,
- EnableMpegtsM2TsMode = false,
- TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
- Context = EncodingContext.Static,
- StreamOptions = new Dictionary<string, string>(),
- EnableAdaptiveBitrateStreaming = true
- };
+ var mediaSource = info.MediaSources[0];
+ if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
+ {
+ return Redirect(mediaSource.Path);
+ }
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
- .ConfigureAwait(false);
- }
+ var isStatic = mediaSource.SupportsDirectStream;
+ if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+ {
+ // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
+ // ffmpeg option -> file extension
+ // mpegts -> ts
+ // fmp4 -> mp4
+ // TODO: remove this when we switch back to the segment muxer
+ var supportedHlsContainers = new[] { "ts", "mp4" };
- var audioStreamingDto = new StreamingRequestDto
+ var dynamicHlsRequestDto = new HlsAudioRequestDto
{
Id = itemId,
- Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
+ Container = ".m3u8",
Static = isStatic,
PlaySessionId = info.PlaySessionId,
+ // fallback to mpegts if device reports some weird value unsupported by hls
+ SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
@@ -220,121 +182,153 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = maxAudioChannels,
- CopyTimestamps = true,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
- SubtitleMethod = SubtitleDeliveryMethod.Embed,
+ SubtitleMethod = SubtitleDeliveryMethod.Hls,
+ RequireAvc = false,
+ DeInterlace = false,
+ RequireNonAnamorphic = false,
+ EnableMpegtsM2TsMode = false,
TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
- Context = EncodingContext.Static
+ Context = EncodingContext.Static,
+ StreamOptions = new Dictionary<string, string>(),
+ EnableAdaptiveBitrateStreaming = true
};
- return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
+ .ConfigureAwait(false);
}
- private DeviceProfile GetDeviceProfile(
- string[] containers,
- string? transcodingContainer,
- string? audioCodec,
- string? transcodingProtocol,
- bool? breakOnNonKeyFrames,
- int? transcodingAudioChannels,
- int? maxAudioSampleRate,
- int? maxAudioBitDepth,
- int? maxAudioChannels)
+ var audioStreamingDto = new StreamingRequestDto
{
- var deviceProfile = new DeviceProfile();
+ Id = itemId,
+ Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
+ Static = isStatic,
+ PlaySessionId = info.PlaySessionId,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = true,
+ AllowAudioStreamCopy = true,
+ AllowVideoStreamCopy = true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames,
+ AudioSampleRate = maxAudioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = maxAudioChannels,
+ CopyTimestamps = true,
+ StartTimeTicks = startTimeTicks,
+ SubtitleMethod = SubtitleDeliveryMethod.Embed,
+ TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
+ Context = EncodingContext.Static
+ };
- int len = containers.Length;
- var directPlayProfiles = new DirectPlayProfile[len];
- for (int i = 0; i < len; i++)
- {
- var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
+ return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
+ }
- var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
+ private DeviceProfile GetDeviceProfile(
+ string[] containers,
+ string? transcodingContainer,
+ string? audioCodec,
+ string? transcodingProtocol,
+ bool? breakOnNonKeyFrames,
+ int? transcodingAudioChannels,
+ int? maxAudioSampleRate,
+ int? maxAudioBitDepth,
+ int? maxAudioChannels)
+ {
+ var deviceProfile = new DeviceProfile();
- directPlayProfiles[i] = new DirectPlayProfile
- {
- Type = DlnaProfileType.Audio,
- Container = parts[0],
- AudioCodec = audioCodecs
- };
- }
+ int len = containers.Length;
+ var directPlayProfiles = new DirectPlayProfile[len];
+ for (int i = 0; i < len; i++)
+ {
+ var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
- deviceProfile.DirectPlayProfiles = directPlayProfiles;
+ var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
- deviceProfile.TranscodingProfiles = new[]
+ directPlayProfiles[i] = new DirectPlayProfile
{
- new TranscodingProfile
- {
- Type = DlnaProfileType.Audio,
- Context = EncodingContext.Streaming,
- Container = transcodingContainer ?? "mp3",
- AudioCodec = audioCodec ?? "mp3",
- Protocol = transcodingProtocol ?? "http",
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
- }
+ Type = DlnaProfileType.Audio,
+ Container = parts[0],
+ AudioCodec = audioCodecs
};
+ }
- var codecProfiles = new List<CodecProfile>();
- var conditions = new List<ProfileCondition>();
+ deviceProfile.DirectPlayProfiles = directPlayProfiles;
- if (maxAudioSampleRate.HasValue)
+ deviceProfile.TranscodingProfiles = new[]
+ {
+ new TranscodingProfile
{
- // codec profile
- conditions.Add(
- new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioSampleRate,
- Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
- });
+ Type = DlnaProfileType.Audio,
+ Context = EncodingContext.Streaming,
+ Container = transcodingContainer ?? "mp3",
+ AudioCodec = audioCodec ?? "mp3",
+ Protocol = transcodingProtocol ?? "http",
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ MaxAudioChannels = transcodingAudioChannels?.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)
- });
- }
+ var codecProfiles = new List<CodecProfile>();
+ var conditions = new List<ProfileCondition>();
- if (maxAudioChannels.HasValue)
- {
- // codec profile
- conditions.Add(
- new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioChannels,
- Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
+ if (maxAudioSampleRate.HasValue)
+ {
+ // codec profile
+ conditions.Add(
+ new ProfileCondition
+ {
+ Condition = ProfileConditionType.LessThanEqual,
+ IsRequired = false,
+ Property = ProfileConditionValue.AudioSampleRate,
+ Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
+ });
+ }
- if (conditions.Count > 0)
- {
- // codec profile
- codecProfiles.Add(
- new CodecProfile
- {
- Type = CodecType.Audio,
- Container = string.Join(',', containers),
- Conditions = conditions.ToArray()
- });
- }
+ if (maxAudioBitDepth.HasValue)
+ {
+ // codec profile
+ conditions.Add(
+ new ProfileCondition
+ {
+ Condition = ProfileConditionType.LessThanEqual,
+ IsRequired = false,
+ Property = ProfileConditionValue.AudioBitDepth,
+ Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
+ });
+ }
- deviceProfile.CodecProfiles = codecProfiles.ToArray();
+ if (maxAudioChannels.HasValue)
+ {
+ // codec profile
+ conditions.Add(
+ new ProfileCondition
+ {
+ Condition = ProfileConditionType.LessThanEqual,
+ IsRequired = false,
+ Property = ProfileConditionValue.AudioChannels,
+ Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
+ });
+ }
- return deviceProfile;
+ if (conditions.Count > 0)
+ {
+ // codec profile
+ codecProfiles.Add(
+ new CodecProfile
+ {
+ Type = CodecType.Audio,
+ Container = string.Join(',', containers),
+ Conditions = conditions.ToArray()
+ });
}
+
+ deviceProfile.CodecProfiles = codecProfiles.ToArray();
+
+ return deviceProfile;
}
}
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 568224a42..b0973b8a1 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -25,564 +25,576 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// User controller.
+/// </summary>
+[Route("Users")]
+public class UserController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly INetworkManager _networkManager;
+ private readonly IDeviceManager _deviceManager;
+ private readonly IAuthorizationContext _authContext;
+ private readonly IServerConfigurationManager _config;
+ private readonly ILogger _logger;
+ private readonly IQuickConnect _quickConnectManager;
+
/// <summary>
- /// User controller.
+ /// Initializes a new instance of the <see cref="UserController"/> class.
/// </summary>
- [Route("Users")]
- public class UserController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <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>
+ /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
+ public UserController(
+ IUserManager userManager,
+ ISessionManager sessionManager,
+ INetworkManager networkManager,
+ IDeviceManager deviceManager,
+ IAuthorizationContext authContext,
+ IServerConfigurationManager config,
+ ILogger<UserController> logger,
+ IQuickConnect quickConnectManager)
{
- private readonly IUserManager _userManager;
- private readonly ISessionManager _sessionManager;
- private readonly INetworkManager _networkManager;
- private readonly IDeviceManager _deviceManager;
- private readonly IAuthorizationContext _authContext;
- private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
- private readonly IQuickConnect _quickConnectManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UserController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <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>
- /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
- public UserController(
- IUserManager userManager,
- ISessionManager sessionManager,
- INetworkManager networkManager,
- IDeviceManager deviceManager,
- IAuthorizationContext authContext,
- IServerConfigurationManager config,
- ILogger<UserController> logger,
- IQuickConnect quickConnectManager)
- {
- _userManager = userManager;
- _sessionManager = sessionManager;
- _networkManager = networkManager;
- _deviceManager = deviceManager;
- _authContext = authContext;
- _config = config;
- _logger = logger;
- _quickConnectManager = quickConnectManager;
- }
-
- /// <summary>
- /// Gets a list of users.
- /// </summary>
- /// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
- /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
- /// <response code="200">Users returned.</response>
- /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
- [HttpGet]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<UserDto>> GetUsers(
- [FromQuery] bool? isHidden,
- [FromQuery] bool? isDisabled)
- {
- var users = Get(isHidden, isDisabled, false, false);
- return Ok(users);
- }
-
- /// <summary>
- /// Gets a list of publicly visible users for display on a login screen.
- /// </summary>
- /// <response code="200">Public users returned.</response>
- /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
- [HttpGet("Public")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
- {
- // If the startup wizard hasn't been completed then just return all users
- if (!_config.Configuration.IsStartupWizardCompleted)
- {
- return Ok(Get(false, false, false, false));
- }
+ _userManager = userManager;
+ _sessionManager = sessionManager;
+ _networkManager = networkManager;
+ _deviceManager = deviceManager;
+ _authContext = authContext;
+ _config = config;
+ _logger = logger;
+ _quickConnectManager = quickConnectManager;
+ }
- return Ok(Get(false, false, true, true));
+ /// <summary>
+ /// Gets a list of users.
+ /// </summary>
+ /// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
+ /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
+ /// <response code="200">Users returned.</response>
+ /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
+ [HttpGet]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<UserDto>> GetUsers(
+ [FromQuery] bool? isHidden,
+ [FromQuery] bool? isDisabled)
+ {
+ var users = Get(isHidden, isDisabled, false, false);
+ return Ok(users);
+ }
+
+ /// <summary>
+ /// Gets a list of publicly visible users for display on a login screen.
+ /// </summary>
+ /// <response code="200">Public users returned.</response>
+ /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
+ [HttpGet("Public")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
+ {
+ // If the startup wizard hasn't been completed then just return all users
+ if (!_config.Configuration.IsStartupWizardCompleted)
+ {
+ return Ok(Get(false, false, false, false));
}
- /// <summary>
- /// Gets a user by Id.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <response code="200">User returned.</response>
- /// <response code="404">User not found.</response>
- /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
- [HttpGet("{userId}")]
- [Authorize(Policy = Policies.IgnoreParentalControl)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserDto> GetUserById([FromRoute, Required] Guid userId)
+ return Ok(Get(false, false, true, true));
+ }
+
+ /// <summary>
+ /// Gets a user by Id.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">User returned.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
+ [HttpGet("{userId}")]
+ [Authorize(Policy = Policies.IgnoreParentalControl)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<UserDto> GetUserById([FromRoute, Required] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ if (user is null)
{
- var user = _userManager.GetUserById(userId);
+ return NotFound("User not found");
+ }
- if (user is null)
- {
- return NotFound("User not found");
- }
+ var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
+ return result;
+ }
- var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
- return result;
+ /// <summary>
+ /// Deletes a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <response code="204">User deleted.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
+ [HttpDelete("{userId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Deletes a user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <response code="204">User deleted.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
- [HttpDelete("{userId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
- {
- var user = _userManager.GetUserById(userId);
- await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
- await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
- return NoContent();
- }
-
- /// <summary>
- /// Authenticates a user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="pw">The password as plain text.</param>
- /// <response code="200">User authenticated.</response>
- /// <response code="403">Sha1-hashed password only is not allowed.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns>
- [HttpPost("{userId}/Authenticate")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Obsolete("Authenticate with username instead")]
- public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
- [FromRoute, Required] Guid userId,
- [FromQuery, Required] string pw)
- {
- var user = _userManager.GetUserById(userId);
-
- if (user is null)
- {
- return NotFound("User not found");
- }
+ await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
+ await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
+ return NoContent();
+ }
- AuthenticateUserByName request = new AuthenticateUserByName
- {
- Username = user.Username,
- Pw = pw
- };
- return await AuthenticateUserByName(request).ConfigureAwait(false);
+ /// <summary>
+ /// Authenticates a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="pw">The password as plain text.</param>
+ /// <response code="200">User authenticated.</response>
+ /// <response code="403">Sha1-hashed password only is not allowed.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns>
+ [HttpPost("{userId}/Authenticate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Authenticate with username instead")]
+ public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
+ [FromRoute, Required] Guid userId,
+ [FromQuery, Required] string pw)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ if (user is null)
+ {
+ return NotFound("User not found");
}
- /// <summary>
- /// Authenticates a user by name.
- /// </summary>
- /// <param name="request">The <see cref="AuthenticateUserByName"/> request.</param>
- /// <response code="200">User authenticated.</response>
- /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
- [HttpPost("AuthenticateByName")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
+ AuthenticateUserByName request = new AuthenticateUserByName
{
- var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ Username = user.Username,
+ Pw = pw
+ };
+ return await AuthenticateUserByName(request).ConfigureAwait(false);
+ }
- try
- {
- var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest
- {
- App = auth.Client,
- AppVersion = auth.Version,
- DeviceId = auth.DeviceId,
- DeviceName = auth.Device,
- Password = request.Pw,
- RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
- Username = request.Username
- }).ConfigureAwait(false);
-
- return result;
- }
- catch (SecurityException e)
+ /// <summary>
+ /// Authenticates a user by name.
+ /// </summary>
+ /// <param name="request">The <see cref="AuthenticateUserByName"/> request.</param>
+ /// <response code="200">User authenticated.</response>
+ /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
+ [HttpPost("AuthenticateByName")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
+ {
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+
+ try
+ {
+ var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest
{
- // rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
- }
+ App = auth.Client,
+ AppVersion = auth.Version,
+ DeviceId = auth.DeviceId,
+ DeviceName = auth.Device,
+ Password = request.Pw,
+ RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
+ Username = request.Username
+ }).ConfigureAwait(false);
+
+ return result;
}
+ catch (SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
+ }
+ }
- /// <summary>
- /// Authenticates a user with quick connect.
- /// </summary>
- /// <param name="request">The <see cref="QuickConnectDto"/> request.</param>
- /// <response code="200">User authenticated.</response>
- /// <response code="400">Missing token.</response>
- /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
- [HttpPost("AuthenticateWithQuickConnect")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+ /// <summary>
+ /// Authenticates a user with quick connect.
+ /// </summary>
+ /// <param name="request">The <see cref="QuickConnectDto"/> request.</param>
+ /// <response code="200">User authenticated.</response>
+ /// <response code="400">Missing token.</response>
+ /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
+ [HttpPost("AuthenticateWithQuickConnect")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+ {
+ try
{
- try
- {
- return _quickConnectManager.GetAuthorizedRequest(request.Secret);
- }
- catch (SecurityException e)
- {
- // rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
- }
+ return _quickConnectManager.GetAuthorizedRequest(request.Secret);
+ }
+ catch (SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
}
+ }
- /// <summary>
- /// Updates a user's password.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
- /// <response code="204">Password successfully reset.</response>
- /// <response code="403">User is not allowed to update the password.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
- [HttpPost("{userId}/Password")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateUserPassword(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UpdateUserPassword request)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
- }
+ /// <summary>
+ /// Updates a user's password.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
+ /// <response code="204">Password successfully reset.</response>
+ /// <response code="403">User is not allowed to update the password.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
+ [HttpPost("{userId}/Password")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateUserPassword(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UpdateUserPassword request)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
+ }
- var user = _userManager.GetUserById(userId);
+ var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return NotFound("User not found");
- }
+ if (user is null)
+ {
+ return NotFound("User not found");
+ }
- if (request.ResetPassword)
- {
- await _userManager.ResetPassword(user).ConfigureAwait(false);
- }
- else
+ if (request.ResetPassword)
+ {
+ await _userManager.ResetPassword(user).ConfigureAwait(false);
+ }
+ else
+ {
+ if (!User.IsInRole(UserRoles.Administrator) || User.GetUserId().Equals(userId))
{
- if (!User.IsInRole(UserRoles.Administrator))
+ var success = await _userManager.AuthenticateUser(
+ user.Username,
+ request.CurrentPw ?? string.Empty,
+ request.CurrentPw ?? string.Empty,
+ HttpContext.GetNormalizedRemoteIp().ToString(),
+ false).ConfigureAwait(false);
+
+ if (success is null)
{
- var success = await _userManager.AuthenticateUser(
- user.Username,
- request.CurrentPw,
- request.CurrentPw,
- HttpContext.GetNormalizedRemoteIp().ToString(),
- false).ConfigureAwait(false);
-
- if (success is null)
- {
- return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
- }
+ return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
}
+ }
- await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
-
- var currentToken = User.GetToken();
+ await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false);
- await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
- }
+ var currentToken = User.GetToken();
- return NoContent();
- }
-
- /// <summary>
- /// Updates a user's easy password.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
- /// <response code="204">Password successfully reset.</response>
- /// <response code="403">User is not allowed to update the password.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
- [HttpPost("{userId}/EasyPassword")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateUserEasyPassword(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UpdateUserEasyPassword request)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
- }
+ await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
+ }
- var user = _userManager.GetUserById(userId);
+ return NoContent();
+ }
- if (user is null)
- {
- return NotFound("User not found");
- }
+ /// <summary>
+ /// Updates a user's easy password.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
+ /// <response code="204">Password successfully reset.</response>
+ /// <response code="403">User is not allowed to update the password.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
+ [HttpPost("{userId}/EasyPassword")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateUserEasyPassword(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UpdateUserEasyPassword request)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
+ }
- if (request.ResetPassword)
- {
- await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
- }
- else
- {
- await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
- }
+ var user = _userManager.GetUserById(userId);
- return NoContent();
- }
-
- /// <summary>
- /// Updates a user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="updateUser">The updated user model.</param>
- /// <response code="204">User updated.</response>
- /// <response code="400">User information was not supplied.</response>
- /// <response code="403">User update forbidden.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
- [HttpPost("{userId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> UpdateUser(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UserDto updateUser)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
- }
+ if (user is null)
+ {
+ return NotFound("User not found");
+ }
- var user = _userManager.GetUserById(userId);
+ if (request.ResetPassword)
+ {
+ await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
+ }
+ else
+ {
+ await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false);
+ }
- if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
- {
- await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
- }
+ return NoContent();
+ }
- await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
-
- return NoContent();
- }
-
- /// <summary>
- /// Updates a user policy.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="newPolicy">The new user policy.</param>
- /// <response code="204">User policy updated.</response>
- /// <response code="400">User policy was not supplied.</response>
- /// <response code="403">User policy update forbidden.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
- [HttpPost("{userId}/Policy")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> UpdateUserPolicy(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UserPolicy newPolicy)
- {
- var user = _userManager.GetUserById(userId);
-
- // If removing admin access
- if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
- {
- if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
- {
- return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
- }
- }
+ /// <summary>
+ /// Updates a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="updateUser">The updated user model.</param>
+ /// <response code="204">User updated.</response>
+ /// <response code="400">User information was not supplied.</response>
+ /// <response code="403">User update forbidden.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
+ [HttpPost("{userId}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> UpdateUser(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UserDto updateUser)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- // If disabling
- if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
- }
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
+ }
- // If disabling
- if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
- {
- if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
- {
- return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
- }
+ if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
+ {
+ await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
+ }
- var currentToken = User.GetToken();
- await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
- }
+ await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
- await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
+ /// <summary>
+ /// Updates a user policy.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="newPolicy">The new user policy.</param>
+ /// <response code="204">User policy updated.</response>
+ /// <response code="400">User policy was not supplied.</response>
+ /// <response code="403">User policy update forbidden.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
+ [HttpPost("{userId}/Policy")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> UpdateUserPolicy(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UserPolicy newPolicy)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Updates a user configuration.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="userConfig">The new user configuration.</param>
- /// <response code="204">User configuration updated.</response>
- /// <response code="403">User configuration update forbidden.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("{userId}/Configuration")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> UpdateUserConfiguration(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UserConfiguration userConfig)
+ // If removing admin access
+ if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
{
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
- return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
}
-
- await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
-
- return NoContent();
}
- /// <summary>
- /// Creates a user.
- /// </summary>
- /// <param name="request">The create user by name request body.</param>
- /// <response code="200">User created.</response>
- /// <returns>An <see cref="UserDto"/> of the new user.</returns>
- [HttpPost("New")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
+ // If disabling
+ if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
{
- var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
+ return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
+ }
- // no need to authenticate password for new user
- if (request.Password is not null)
+ // If disabling
+ if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
+ {
+ if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
{
- await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
- var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
-
- return result;
+ var currentToken = User.GetToken();
+ await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
- /// <summary>
- /// Initiates the forgot password process for a local user.
- /// </summary>
- /// <param name="forgotPasswordRequest">The forgot password request containing the entered username.</param>
- /// <response code="200">Password reset process started.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
- [HttpPost("ForgotPassword")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
+ await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Updates a user configuration.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userConfig">The new user configuration.</param>
+ /// <response code="204">User configuration updated.</response>
+ /// <response code="403">User configuration update forbidden.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("{userId}/Configuration")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> UpdateUserConfiguration(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UserConfiguration userConfig)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
{
- var ip = HttpContext.GetNormalizedRemoteIp();
- var isLocal = HttpContext.IsLocal()
- || _networkManager.IsInLocalNetwork(ip);
+ return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
+ }
- if (isLocal)
- {
- _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
- }
+ await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
- var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
+ return NoContent();
+ }
- return result;
+ /// <summary>
+ /// Creates a user.
+ /// </summary>
+ /// <param name="request">The create user by name request body.</param>
+ /// <response code="200">User created.</response>
+ /// <returns>An <see cref="UserDto"/> of the new user.</returns>
+ [HttpPost("New")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
+ {
+ var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
+
+ // no need to authenticate password for new user
+ if (request.Password is not null)
+ {
+ await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
- /// <summary>
- /// Redeems a forgot password pin.
- /// </summary>
- /// <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, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
+ var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
+
+ return result;
+ }
+
+ /// <summary>
+ /// Initiates the forgot password process for a local user.
+ /// </summary>
+ /// <param name="forgotPasswordRequest">The forgot password request containing the entered username.</param>
+ /// <response code="200">Password reset process started.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
+ [HttpPost("ForgotPassword")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
+ {
+ var ip = HttpContext.GetNormalizedRemoteIp();
+ var isLocal = HttpContext.IsLocal()
+ || _networkManager.IsInLocalNetwork(ip);
+
+ if (isLocal)
{
- var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
- return result;
+ _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
}
- /// <summary>
- /// Gets the user based on auth token.
- /// </summary>
- /// <response code="200">User returned.</response>
- /// <response code="400">Token is not owned by a user.</response>
- /// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
- [HttpGet("Me")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public ActionResult<UserDto> GetCurrentUser()
- {
- var userId = User.GetUserId();
- if (userId.Equals(default))
- {
- return BadRequest();
- }
+ var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
- var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return BadRequest();
- }
+ return result;
+ }
+
+ /// <summary>
+ /// Redeems a forgot password pin.
+ /// </summary>
+ /// <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, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
+ {
+ var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
+ return result;
+ }
+
+ /// <summary>
+ /// Gets the user based on auth token.
+ /// </summary>
+ /// <response code="200">User returned.</response>
+ /// <response code="400">Token is not owned by a user.</response>
+ /// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
+ [HttpGet("Me")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public ActionResult<UserDto> GetCurrentUser()
+ {
+ var userId = User.GetUserId();
+ if (userId.Equals(default))
+ {
+ return BadRequest();
+ }
- return _userManager.GetUserDto(user);
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return BadRequest();
}
- private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
+ return _userManager.GetUserDto(user);
+ }
+
+ private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
+ {
+ var users = _userManager.Users;
+
+ if (isDisabled.HasValue)
{
- var users = _userManager.Users;
+ users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value);
+ }
- if (isDisabled.HasValue)
- {
- users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value);
- }
+ if (isHidden.HasValue)
+ {
+ users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value);
+ }
- if (isHidden.HasValue)
- {
- users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value);
- }
+ if (filterByDevice)
+ {
+ var deviceId = User.GetDeviceId();
- if (filterByDevice)
+ if (!string.IsNullOrWhiteSpace(deviceId))
{
- var deviceId = User.GetDeviceId();
-
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
- }
+ users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
}
+ }
- if (filterByNetwork)
+ if (filterByNetwork)
+ {
+ if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
{
- if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
- {
- users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
- }
+ users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
}
+ }
- var result = users
- .OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
+ var result = users
+ .OrderBy(u => u.Username)
+ .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
- return result;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index cd21c5f6f..2c4fe9186 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -4,10 +4,9 @@ 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.ModelBinders;
-using Jellyfin.Api.Models.UserDtos;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -23,406 +22,564 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// User library controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class UserLibraryController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepository;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserViewManager _userViewManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILyricManager _lyricManager;
+
/// <summary>
- /// User library controller.
+ /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class UserLibraryController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
+ public UserLibraryController(
+ IUserManager userManager,
+ IUserDataManager userDataRepository,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IUserViewManager userViewManager,
+ IFileSystem fileSystem,
+ ILyricManager lyricManager)
{
- private readonly IUserManager _userManager;
- private readonly IUserDataManager _userDataRepository;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IUserViewManager _userViewManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILyricManager _lyricManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
- public UserLibraryController(
- IUserManager userManager,
- IUserDataManager userDataRepository,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IUserViewManager userViewManager,
- IFileSystem fileSystem,
- ILyricManager lyricManager)
- {
- _userManager = userManager;
- _userDataRepository = userDataRepository;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _userViewManager = userViewManager;
- _fileSystem = fileSystem;
- _lyricManager = lyricManager;
- }
-
- /// <summary>
- /// Gets an item from a user's library.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Item returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the d item.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- var user = _userManager.GetUserById(userId);
-
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
- await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
-
- var dtoOptions = new DtoOptions().AddClientFields(User);
-
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- /// <summary>
- /// Gets the root folder from a user's library.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <response code="200">Root folder returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
- [HttpGet("Users/{userId}/Items/Root")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetRootFolder([FromRoute, Required] Guid userId)
- {
- var user = _userManager.GetUserById(userId);
- var item = _libraryManager.GetUserRootFolder();
- var dtoOptions = new DtoOptions().AddClientFields(User);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- /// <summary>
- /// Gets intros to play before the main media item plays.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Intros returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}/Intros")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- var user = _userManager.GetUserById(userId);
-
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
- var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
-
- return new QueryResult<BaseItemDto>(dtos);
- }
-
- /// <summary>
- /// Marks an item as a favorite.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Item marked as favorite.</response>
- /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- return MarkFavorite(userId, itemId, true);
- }
-
- /// <summary>
- /// Unmarks item as a favorite.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Item unmarked as favorite.</response>
- /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- return MarkFavorite(userId, itemId, false);
- }
-
- /// <summary>
- /// Deletes a user's saved personal rating for an item.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Personal rating removed.</response>
- /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- return UpdateUserItemRatingInternal(userId, itemId, null);
- }
-
- /// <summary>
- /// Updates a user's rating for an item.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
- /// <response code="200">Item rating updated.</response>
- /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpPost("Users/{userId}/Items/{itemId}/Rating")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes)
- {
- return UpdateUserItemRatingInternal(userId, itemId, likes);
- }
-
- /// <summary>
- /// Gets local trailers for an item.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
- /// <returns>The items local trailers.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- var user = _userManager.GetUserById(userId);
-
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
- var dtoOptions = new DtoOptions().AddClientFields(User);
-
- if (item is IHasTrailers hasTrailers)
- {
- var trailers = hasTrailers.LocalTrailers;
- return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
- }
+ _userManager = userManager;
+ _userDataRepository = userDataRepository;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _userViewManager = userViewManager;
+ _fileSystem = fileSystem;
+ _lyricManager = lyricManager;
+ }
- return Ok(item.GetExtras()
- .Where(e => e.ExtraType == ExtraType.Trailer)
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
- }
-
- /// <summary>
- /// Gets special features for an item.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Special features returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- var user = _userManager.GetUserById(userId);
-
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
- var dtoOptions = new DtoOptions().AddClientFields(User);
-
- return Ok(item
- .GetExtras()
- .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
- }
-
- /// <summary>
- /// Gets latest media.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isPlayed">Filter by items that are played, or not.</param>
- /// <param name="enableImages">Optional. include image information in output.</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>
- /// <param name="enableUserData">Optional. include user data.</param>
- /// <param name="limit">Return item limit.</param>
- /// <param name="groupItems">Whether or not to group items into a parent container.</param>
- /// <response code="200">Latest media returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
- [HttpGet("Users/{userId}/Items/Latest")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
- [FromRoute, Required] Guid userId,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isPlayed,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] int limit = 20,
- [FromQuery] bool groupItems = true)
- {
- var user = _userManager.GetUserById(userId);
-
- if (!isPlayed.HasValue)
- {
- if (user.HidePlayedInLatest)
- {
- isPlayed = false;
- }
- }
+ /// <summary>
+ /// Gets an item from a user's library.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Item returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the item.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
- var list = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- GroupItems = groupItems,
- IncludeItemTypes = includeItemTypes,
- IsPlayed = isPlayed,
- Limit = limit,
- ParentId = parentId ?? Guid.Empty,
- UserId = userId,
- },
- dtoOptions);
-
- var dtos = list.Select(i =>
- {
- var item = i.Item2[0];
- var childCount = 0;
+ if (item is null)
+ {
+ return NotFound();
+ }
- if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
- {
- item = i.Item1;
- childCount = i.Item2.Count;
- }
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
+ }
+
+ await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
+
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
+
+ /// <summary>
+ /// Gets the root folder from a user's library.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <response code="200">Root folder returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
+ [HttpGet("Users/{userId}/Items/Root")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<BaseItemDto> GetRootFolder([FromRoute, Required] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ var item = _libraryManager.GetUserRootFolder();
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- dto.ChildCount = childCount;
+ /// <summary>
+ /// Gets intros to play before the main media item plays.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Intros returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}/Intros")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- return dto;
- });
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
- return Ok(dtos);
+ if (item is null)
+ {
+ return NotFound();
}
- private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
{
- if (item is Person)
- {
- var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
- var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
+ }
- if (!hasMetdata)
- {
- var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ForceSave = performFullRefresh
- };
-
- await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
- }
- }
+ var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
+
+ return new QueryResult<BaseItemDto>(dtos);
+ }
+
+ /// <summary>
+ /// Marks an item as a favorite.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Item marked as favorite.</response>
+ /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+ [HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
+ }
+
+ return MarkFavorite(user, item, true);
+ }
+
+ /// <summary>
+ /// Unmarks item as a favorite.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Item unmarked as favorite.</response>
+ /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+ [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
+ }
+
+ return MarkFavorite(user, item, false);
+ }
+
+ /// <summary>
+ /// Deletes a user's saved personal rating for an item.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Personal rating removed.</response>
+ /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+ [HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
- /// <summary>
- /// Marks the favorite.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="itemId">The item id.</param>
- /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
- private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite)
+ return UpdateUserItemRatingInternal(user, item, null);
+ }
+
+ /// <summary>
+ /// Updates a user's rating for an item.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
+ /// <response code="200">Item rating updated.</response>
+ /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+ [HttpPost("Users/{userId}/Items/{itemId}/Rating")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
{
- var user = _userManager.GetUserById(userId);
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
+ }
- var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
+ return UpdateUserItemRatingInternal(user, item, likes);
+ }
- // Get the user data for this item
- var data = _userDataRepository.GetUserData(user, item);
+ /// <summary>
+ /// Gets local trailers for an item.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
+ /// <returns>The items local trailers.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- // Set favorite status
- data.IsFavorite = isFavorite;
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return _userDataRepository.GetUserDataDto(item, user);
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
- /// <summary>
- /// Updates the user item rating.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="itemId">The item id.</param>
- /// <param name="likes">if set to <c>true</c> [likes].</param>
- private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes)
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ if (item is IHasTrailers hasTrailers)
{
- var user = _userManager.GetUserById(userId);
+ var trailers = hasTrailers.LocalTrailers;
+ return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
+ }
- var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
+ return Ok(item.GetExtras()
+ .Where(e => e.ExtraType == ExtraType.Trailer)
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
+ }
- // Get the user data for this item
- var data = _userDataRepository.GetUserData(user, item);
+ /// <summary>
+ /// Gets special features for an item.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Special features returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- data.Likes = likes;
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return _userDataRepository.GetUserDataDto(item, user);
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
- /// <summary>
- /// Gets an item's lyrics.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Lyrics returned.</response>
- /// <response code="404">Something went wrong. No Lyrics will be returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<LyricResponse>> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+
+ return Ok(item
+ .GetExtras()
+ .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
+ }
+
+ /// <summary>
+ /// Gets latest media.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isPlayed">Filter by items that are played, or not.</param>
+ /// <param name="enableImages">Optional. include image information in output.</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>
+ /// <param name="enableUserData">Optional. include user data.</param>
+ /// <param name="limit">Return item limit.</param>
+ /// <param name="groupItems">Whether or not to group items into a parent container.</param>
+ /// <response code="200">Latest media returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
+ [HttpGet("Users/{userId}/Items/Latest")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
+ [FromRoute, Required] Guid userId,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isPlayed,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int limit = 20,
+ [FromQuery] bool groupItems = true)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
{
- var user = _userManager.GetUserById(userId);
+ return NotFound();
+ }
- if (user is null)
+ if (!isPlayed.HasValue)
+ {
+ if (user.HidePlayedInLatest)
{
- return NotFound();
+ isPlayed = false;
}
+ }
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- if (item is null)
+ var list = _userViewManager.GetLatestItems(
+ new LatestItemsQuery
{
- return NotFound();
+ GroupItems = groupItems,
+ IncludeItemTypes = includeItemTypes,
+ IsPlayed = isPlayed,
+ Limit = limit,
+ ParentId = parentId ?? Guid.Empty,
+ UserId = userId,
+ },
+ dtoOptions);
+
+ var dtos = list.Select(i =>
+ {
+ var item = i.Item2[0];
+ var childCount = 0;
+
+ if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
+ {
+ item = i.Item1;
+ childCount = i.Item2.Count;
}
- var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
- if (result is not null)
+ var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
+
+ dto.ChildCount = childCount;
+
+ return dto;
+ });
+
+ return Ok(dtos);
+ }
+
+ private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
+ {
+ if (item is Person)
+ {
+ var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
+ var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+
+ if (!hasMetdata)
{
- return Ok(result);
+ var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ForceSave = performFullRefresh
+ };
+
+ await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
}
+ }
+ }
+
+ /// <summary>
+ /// Marks the favorite.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
+ private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite)
+ {
+ // Get the user data for this item
+ var data = _userDataRepository.GetUserData(user, item);
+
+ // Set favorite status
+ data.IsFavorite = isFavorite;
+
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
+
+ /// <summary>
+ /// Updates the user item rating.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="likes">if set to <c>true</c> [likes].</param>
+ private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
+ {
+ // Get the user data for this item
+ var data = _userDataRepository.GetUserData(user, item);
+
+ data.Likes = likes;
+
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
+
+ /// <summary>
+ /// Gets an item's lyrics.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Lyrics returned.</response>
+ /// <response code="404">Something went wrong. No Lyrics will be returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<LyricResponse>> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ if (user is null)
+ {
+ return NotFound();
+ }
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ if (item is null)
+ {
return NotFound();
}
+
+ if (item is not UserRootFolder
+ // Check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
+ }
+
+ var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
+ if (result is not null)
+ {
+ return Ok(result);
+ }
+
+ return NotFound();
}
}
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 3aeb444df..838b43234 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
@@ -17,122 +16,121 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// User views controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class UserViewsController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly IUserViewManager _userViewManager;
+ private readonly IDtoService _dtoService;
+ private readonly ILibraryManager _libraryManager;
+
/// <summary>
- /// User views controller.
+ /// Initializes a new instance of the <see cref="UserViewsController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class UserViewsController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public UserViewsController(
+ IUserManager userManager,
+ IUserViewManager userViewManager,
+ IDtoService dtoService,
+ ILibraryManager libraryManager)
{
- private readonly IUserManager _userManager;
- private readonly IUserViewManager _userViewManager;
- private readonly IDtoService _dtoService;
- private readonly ILibraryManager _libraryManager;
+ _userManager = userManager;
+ _userViewManager = userViewManager;
+ _dtoService = dtoService;
+ _libraryManager = libraryManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="UserViewsController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public UserViewsController(
- IUserManager userManager,
- IUserViewManager userViewManager,
- IDtoService dtoService,
- ILibraryManager libraryManager)
+ /// <summary>
+ /// Get user views.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
+ /// <param name="presetViews">Preset views.</param>
+ /// <param name="includeHidden">Whether or not to include hidden content.</param>
+ /// <response code="200">User views returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
+ [HttpGet("Users/{userId}/Views")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public QueryResult<BaseItemDto> GetUserViews(
+ [FromRoute, Required] Guid userId,
+ [FromQuery] bool? includeExternalContent,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
+ [FromQuery] bool includeHidden = false)
+ {
+ var query = new UserViewQuery
{
- _userManager = userManager;
- _userViewManager = userViewManager;
- _dtoService = dtoService;
- _libraryManager = libraryManager;
- }
+ UserId = userId,
+ IncludeHidden = includeHidden
+ };
- /// <summary>
- /// Get user views.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
- /// <param name="presetViews">Preset views.</param>
- /// <param name="includeHidden">Whether or not to include hidden content.</param>
- /// <response code="200">User views returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
- [HttpGet("Users/{userId}/Views")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public QueryResult<BaseItemDto> GetUserViews(
- [FromRoute, Required] Guid userId,
- [FromQuery] bool? includeExternalContent,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
- [FromQuery] bool includeHidden = false)
+ if (includeExternalContent.HasValue)
{
- var query = new UserViewQuery
- {
- UserId = userId,
- IncludeHidden = includeHidden
- };
+ query.IncludeExternalContent = includeExternalContent.Value;
+ }
- if (includeExternalContent.HasValue)
- {
- query.IncludeExternalContent = includeExternalContent.Value;
- }
+ if (presetViews.Length != 0)
+ {
+ query.PresetViews = presetViews;
+ }
- if (presetViews.Length != 0)
- {
- query.PresetViews = presetViews;
- }
+ var folders = _userViewManager.GetUserViews(query);
- var folders = _userViewManager.GetUserViews(query);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var fields = dtoOptions.Fields.ToList();
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var fields = dtoOptions.Fields.ToList();
+ fields.Add(ItemFields.PrimaryImageAspectRatio);
+ fields.Add(ItemFields.DisplayPreferencesId);
+ fields.Remove(ItemFields.BasicSyncInfo);
+ dtoOptions.Fields = fields.ToArray();
- fields.Add(ItemFields.PrimaryImageAspectRatio);
- fields.Add(ItemFields.DisplayPreferencesId);
- fields.Remove(ItemFields.BasicSyncInfo);
- dtoOptions.Fields = fields.ToArray();
+ var user = _userManager.GetUserById(userId);
- var user = _userManager.GetUserById(userId);
+ var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
+ .ToArray();
- var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
- .ToArray();
+ return new QueryResult<BaseItemDto>(dtos);
+ }
- return new QueryResult<BaseItemDto>(dtos);
+ /// <summary>
+ /// Get user view grouping options.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <response code="200">User view grouping options returned.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the user view grouping options
+ /// or a <see cref="NotFoundResult"/> if user not found.
+ /// </returns>
+ [HttpGet("Users/{userId}/GroupingOptions")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute, Required] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get user view grouping options.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <response code="200">User view grouping options returned.</response>
- /// <response code="404">User not found.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the user view grouping options
- /// or a <see cref="NotFoundResult"/> if user not found.
- /// </returns>
- [HttpGet("Users/{userId}/GroupingOptions")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute, Required] Guid userId)
- {
- var user = _userManager.GetUserById(userId);
- if (user is null)
+ return Ok(_libraryManager.GetUserRootFolder()
+ .GetChildren(user, true)
+ .OfType<Folder>()
+ .Where(UserView.IsEligibleForGrouping)
+ .Select(i => new SpecialViewOptionDto
{
- return NotFound();
- }
-
- return Ok(_libraryManager.GetUserRootFolder()
- .GetChildren(user, true)
- .OfType<Folder>()
- .Where(UserView.IsEligibleForGrouping)
- .Select(i => new SpecialViewOptionDto
- {
- Name = i.Name,
- Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
- })
- .OrderBy(i => i.Name)
- .AsEnumerable());
- }
+ Name = i.Name,
+ Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
+ })
+ .OrderBy(i => i.Name)
+ .AsEnumerable());
}
}
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index bb3162614..23b9ba46f 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -10,73 +10,72 @@ using MediaBrowser.Controller.MediaEncoding;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Attachments controller.
+/// </summary>
+[Route("Videos")]
+public class VideoAttachmentsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IAttachmentExtractor _attachmentExtractor;
+
/// <summary>
- /// Attachments controller.
+ /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
/// </summary>
- [Route("Videos")]
- public class VideoAttachmentsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+ public VideoAttachmentsController(
+ ILibraryManager libraryManager,
+ IAttachmentExtractor attachmentExtractor)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IAttachmentExtractor _attachmentExtractor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
- public VideoAttachmentsController(
- ILibraryManager libraryManager,
- IAttachmentExtractor attachmentExtractor)
- {
- _libraryManager = libraryManager;
- _attachmentExtractor = attachmentExtractor;
- }
+ _libraryManager = libraryManager;
+ _attachmentExtractor = attachmentExtractor;
+ }
- /// <summary>
- /// Get video attachment.
- /// </summary>
- /// <param name="videoId">Video ID.</param>
- /// <param name="mediaSourceId">Media Source ID.</param>
- /// <param name="index">Attachment Index.</param>
- /// <response code="200">Attachment retrieved.</response>
- /// <response code="404">Video or attachment not found.</response>
- /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
- [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
- [ProducesFile(MediaTypeNames.Application.Octet)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> GetAttachment(
- [FromRoute, Required] Guid videoId,
- [FromRoute, Required] string mediaSourceId,
- [FromRoute, Required] int index)
+ /// <summary>
+ /// Get video attachment.
+ /// </summary>
+ /// <param name="videoId">Video ID.</param>
+ /// <param name="mediaSourceId">Media Source ID.</param>
+ /// <param name="index">Attachment Index.</param>
+ /// <response code="200">Attachment retrieved.</response>
+ /// <response code="404">Video or attachment not found.</response>
+ /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
+ [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
+ [ProducesFile(MediaTypeNames.Application.Octet)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetAttachment(
+ [FromRoute, Required] Guid videoId,
+ [FromRoute, Required] string mediaSourceId,
+ [FromRoute, Required] int index)
+ {
+ try
{
- try
+ var item = _libraryManager.GetItemById(videoId);
+ if (item is null)
{
- var item = _libraryManager.GetItemById(videoId);
- if (item is null)
- {
- return NotFound();
- }
+ return NotFound();
+ }
- var (attachment, stream) = await _attachmentExtractor.GetAttachment(
- item,
- mediaSourceId,
- index,
- CancellationToken.None)
- .ConfigureAwait(false);
+ var (attachment, stream) = await _attachmentExtractor.GetAttachment(
+ item,
+ mediaSourceId,
+ index,
+ CancellationToken.None)
+ .ConfigureAwait(false);
- var contentType = string.IsNullOrWhiteSpace(attachment.MimeType)
- ? MediaTypeNames.Application.Octet
- : attachment.MimeType;
+ var contentType = string.IsNullOrWhiteSpace(attachment.MimeType)
+ ? MediaTypeNames.Application.Octet
+ : attachment.MimeType;
- return new FileStreamResult(stream, contentType);
- }
- catch (ResourceNotFoundException e)
- {
- return NotFound(e.Message);
- }
+ return new FileStreamResult(stream, contentType);
+ }
+ catch (ResourceNotFoundException e)
+ {
+ return NotFound(e.Message);
}
}
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 64d8fb498..c0ec646ed 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -21,7 +21,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -32,644 +31,649 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The videos controller.
+/// </summary>
+public class VideosController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly EncodingHelper _encodingHelper;
+
+ private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideosController"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <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="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,
+ IDtoService dtoService,
+ IDlnaManager dlnaManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ IHttpClientFactory httpClientFactory,
+ EncodingHelper encodingHelper)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _dlnaManager = dlnaManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _httpClientFactory = httpClientFactory;
+ _encodingHelper = encodingHelper;
+ }
+
/// <summary>
- /// The videos controller.
+ /// Gets additional parts for a video.
/// </summary>
- public class VideosController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Additional parts returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns>
+ [HttpGet("{itemId}/AdditionalParts")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly IDlnaManager _dlnaManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceManager _deviceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly EncodingHelper _encodingHelper;
-
- private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideosController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <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="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,
- IDtoService dtoService,
- IDlnaManager dlnaManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- IHttpClientFactory httpClientFactory,
- EncodingHelper encodingHelper)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var item = itemId.Equals(default)
+ ? (userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
+
+ var dtoOptions = new DtoOptions();
+ dtoOptions = dtoOptions.AddClientFields(User);
+
+ BaseItemDto[] items;
+ if (item is Video video)
{
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- _dlnaManager = dlnaManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _httpClientFactory = httpClientFactory;
- _encodingHelper = encodingHelper;
+ items = video.GetAdditionalParts()
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
+ .ToArray();
}
-
- /// <summary>
- /// Gets additional parts for a video.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Additional parts returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns>
- [HttpGet("{itemId}/AdditionalParts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
+ else
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ items = Array.Empty<BaseItemDto>();
+ }
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
+ var result = new QueryResult<BaseItemDto>(items);
+ return result;
+ }
- var dtoOptions = new DtoOptions();
- dtoOptions = dtoOptions.AddClientFields(User);
+ /// <summary>
+ /// Removes alternate video sources.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="204">Alternate sources deleted.</response>
+ /// <response code="404">Video not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
+ [HttpDelete("{itemId}/AlternateSources")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
- BaseItemDto[] items;
- if (item is Video video)
- {
- items = video.GetAdditionalParts()
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
- .ToArray();
- }
- else
- {
- items = Array.Empty<BaseItemDto>();
- }
+ if (video is null)
+ {
+ return NotFound("The video either does not exist or the id does not belong to a video.");
+ }
- var result = new QueryResult<BaseItemDto>(items);
- return result;
+ if (video.LinkedAlternateVersions.Length == 0)
+ {
+ video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId);
}
- /// <summary>
- /// Removes alternate video sources.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="204">Alternate sources deleted.</response>
- /// <response code="404">Video not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
- [HttpDelete("{itemId}/AlternateSources")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
+ if (video is null)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ return NotFound();
+ }
- if (video is null)
- {
- return NotFound("The video either does not exist or the id does not belong to a video.");
- }
+ foreach (var link in video.GetLinkedAlternateVersions())
+ {
+ link.SetPrimaryVersionId(null);
+ link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- if (video.LinkedAlternateVersions.Length == 0)
- {
- video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId);
- }
+ await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ }
- foreach (var link in video.GetLinkedAlternateVersions())
- {
- link.SetPrimaryVersionId(null);
- link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ video.SetPrimaryVersionId(null);
+ await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- }
+ return NoContent();
+ }
- video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- video.SetPrimaryVersionId(null);
- await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ /// <summary>
+ /// Merges videos into a single record.
+ /// </summary>
+ /// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
+ /// <response code="204">Videos merged.</response>
+ /// <response code="400">Supply at least 2 video ids.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
+ [HttpPost("MergeVersions")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ var items = ids
+ .Select(i => _libraryManager.GetItemById(i))
+ .OfType<Video>()
+ .OrderBy(i => i.Id)
+ .ToList();
- return NoContent();
+ if (items.Count < 2)
+ {
+ return BadRequest("Please supply at least two videos to merge.");
}
- /// <summary>
- /// Merges videos into a single record.
- /// </summary>
- /// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
- /// <response code="204">Videos merged.</response>
- /// <response code="400">Supply at least 2 video ids.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
- [HttpPost("MergeVersions")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
+ if (primaryVersion is null)
{
- var items = ids
- .Select(i => _libraryManager.GetItemById(i))
- .OfType<Video>()
- .OrderBy(i => i.Id)
- .ToList();
-
- if (items.Count < 2)
- {
- return BadRequest("Please supply at least two videos to merge.");
- }
-
- var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
- if (primaryVersion is null)
- {
- primaryVersion = items
- .OrderBy(i =>
+ primaryVersion = items
+ .OrderBy(i =>
+ {
+ if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
{
- if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
- {
- return 1;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0)
- .First();
- }
+ return 1;
+ }
- var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
+ return 0;
+ })
+ .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0)
+ .First();
+ }
- foreach (var item in items.Where(i => !i.Id.Equals(primaryVersion.Id)))
- {
- item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
+ var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
- await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ foreach (var item in items.Where(i => !i.Id.Equals(primaryVersion.Id)))
+ {
+ item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
- if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
- {
- alternateVersionsOfPrimary.Add(new LinkedChild
- {
- Path = item.Path,
- ItemId = item.Id
- });
- }
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- foreach (var linkedItem in item.LinkedAlternateVersions)
+ if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
+ {
+ alternateVersionsOfPrimary.Add(new LinkedChild
{
- if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
- {
- alternateVersionsOfPrimary.Add(linkedItem);
- }
- }
+ Path = item.Path,
+ ItemId = item.Id
+ });
+ }
- if (item.LinkedAlternateVersions.Length > 0)
+ foreach (var linkedItem in item.LinkedAlternateVersions)
+ {
+ if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
{
- item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ alternateVersionsOfPrimary.Add(linkedItem);
}
}
- primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
- await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
+ if (item.LinkedAlternateVersions.Length > 0)
+ {
+ item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ }
}
- /// <summary>
- /// Gets a video stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <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")]
- [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesVideoFile]
- public async Task<ActionResult> GetVideoStream(
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
+ await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets a video stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <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")]
+ [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
+ public async Task<ActionResult> GetVideoStream(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new VideoRequestDto
{
- var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- var streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
-
- var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- _transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- if (@static.HasValue && @static.Value && state.DirectStreamProvider is not null)
- {
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
+ Id = itemId,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ _transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
- var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
- if (liveStreamInfo is null)
- {
- return NotFound();
- }
+ if (@static.HasValue && @static.Value && state.DirectStreamProvider is not null)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
- // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return File(liveStream, MimeTypes.GetMimeType("file.ts"));
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo is null)
+ {
+ return NotFound();
}
- // Static remote stream
- if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
- {
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
+ // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+ return File(liveStream, MimeTypes.GetMimeType("file.ts"));
+ }
- var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false);
- }
+ // Static remote stream
+ if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
- if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
- {
- return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
- }
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false);
+ }
- var outputPath = state.OutputFilePath;
- var outputPathExists = System.IO.File.Exists(outputPath);
+ if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
+ {
+ return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
+ }
- var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob is not null;
+ var outputPath = state.OutputFilePath;
+ var outputPathExists = System.IO.File.Exists(outputPath);
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager);
+ var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+ var isTranscodeCached = outputPathExists && transcodingJob is not null;
- // Static stream
- if (@static.HasValue && @static.Value)
- {
- var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager);
- if (state.MediaSource.IsInfiniteStream)
- {
- var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
- return File(liveStream, contentType);
- }
+ // Static stream
+ if (@static.HasValue && @static.Value)
+ {
+ var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
- return FileStreamResponseHelpers.GetStaticFileResult(
- state.MediaPath,
- contentType);
+ if (state.MediaSource.IsInfiniteStream)
+ {
+ var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return File(liveStream, contentType);
}
- // Need to start ffmpeg (because media can't be returned directly)
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
- return await FileStreamResponseHelpers.GetTranscodedFile(
- state,
- isHeadRequest,
- HttpContext,
- _transcodingJobHelper,
- ffmpegCommandLineArguments,
- _transcodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
+ return FileStreamResponseHelpers.GetStaticFileResult(
+ state.MediaPath,
+ contentType);
}
- /// <summary>
- /// Gets a video stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <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 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>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <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 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>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <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.{container}")]
- [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesVideoFile]
- public Task<ActionResult> GetVideoStreamByContainer(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string container,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
- {
- return GetVideoStream(
- itemId,
- container,
- @static,
- @params,
- tag,
- deviceProfileId,
- playSessionId,
- segmentContainer,
- segmentLength,
- minSegments,
- mediaSourceId,
- deviceId,
- audioCodec,
- enableAutoStreamCopy,
- allowVideoStreamCopy,
- allowAudioStreamCopy,
- breakOnNonKeyFrames,
- audioSampleRate,
- maxAudioBitDepth,
- audioBitRate,
- audioChannels,
- maxAudioChannels,
- profile,
- level,
- framerate,
- maxFramerate,
- copyTimestamps,
- startTimeTicks,
- width,
- height,
- maxWidth,
- maxHeight,
- videoBitRate,
- subtitleStreamIndex,
- subtitleMethod,
- maxRefFrames,
- maxVideoBitDepth,
- requireAvc,
- deInterlace,
- requireNonAnamorphic,
- transcodingMaxAudioChannels,
- cpuCoreLimit,
- liveStreamId,
- enableMpegtsM2TsMode,
- videoCodec,
- subtitleCodec,
- transcodeReasons,
- audioStreamIndex,
- videoStreamIndex,
- context,
- streamOptions);
- }
+ // Need to start ffmpeg (because media can't be returned directly)
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
+ return await FileStreamResponseHelpers.GetTranscodedFile(
+ state,
+ isHeadRequest,
+ HttpContext,
+ _transcodingJobHelper,
+ ffmpegCommandLineArguments,
+ _transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets a video stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <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 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>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <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 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>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <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.{container}")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
+ public Task<ActionResult> GetVideoStreamByContainer(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ return GetVideoStream(
+ itemId,
+ container,
+ @static,
+ @params,
+ tag,
+ deviceProfileId,
+ playSessionId,
+ segmentContainer,
+ segmentLength,
+ minSegments,
+ mediaSourceId,
+ deviceId,
+ audioCodec,
+ enableAutoStreamCopy,
+ allowVideoStreamCopy,
+ allowAudioStreamCopy,
+ breakOnNonKeyFrames,
+ audioSampleRate,
+ maxAudioBitDepth,
+ audioBitRate,
+ audioChannels,
+ maxAudioChannels,
+ profile,
+ level,
+ framerate,
+ maxFramerate,
+ copyTimestamps,
+ startTimeTicks,
+ width,
+ height,
+ maxWidth,
+ maxHeight,
+ videoBitRate,
+ subtitleStreamIndex,
+ subtitleMethod,
+ maxRefFrames,
+ maxVideoBitDepth,
+ requireAvc,
+ deInterlace,
+ requireNonAnamorphic,
+ transcodingMaxAudioChannels,
+ cpuCoreLimit,
+ liveStreamId,
+ enableMpegtsM2TsMode,
+ videoCodec,
+ subtitleCodec,
+ transcodeReasons,
+ audioStreamIndex,
+ videoStreamIndex,
+ context,
+ streamOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index cd85ba221..74370db50 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -2,7 +2,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.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -19,208 +18,209 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Years controller.
+/// </summary>
+[Authorize]
+public class YearsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// Years controller.
+ /// Initializes a new instance of the <see cref="YearsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class YearsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public YearsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="YearsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public YearsController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- }
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Get years.
- /// </summary>
- /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
- /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</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>
- /// <param name="userId">User Id.</param>
- /// <param name="recursive">Search recursively.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <response code="200">Year query returned.</response>
- /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetYears(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [FromQuery] bool recursive = true,
- [FromQuery] bool? enableImages = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ /// <summary>
+ /// Get years.
+ /// </summary>
+ /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</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>
+ /// <param name="userId">User Id.</param>
+ /// <param name="recursive">Search recursively.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <response code="200">Year query returned.</response>
+ /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetYears(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool recursive = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+ User? user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
- var query = new InternalItemsQuery(user)
- {
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- DtoOptions = dtoOptions
- };
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
+ DtoOptions = dtoOptions
+ };
+
+ bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
- bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
+ IList<BaseItem> items;
+ if (parentItem.IsFolder)
+ {
+ var folder = (Folder)parentItem;
- IList<BaseItem> items;
- if (parentItem.IsFolder)
+ if (userId.Equals(default))
{
- var folder = (Folder)parentItem;
-
- if (userId.Equals(default))
- {
- items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
- }
- else
- {
- items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
- }
+ items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
}
else
{
- items = new[] { parentItem }.Where(Filter).ToList();
+ items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
}
+ }
+ else
+ {
+ items = new[] { parentItem }.Where(Filter).ToList();
+ }
- var extractedItems = GetAllItems(items);
+ var extractedItems = GetAllItems(items);
- var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder));
+ var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder));
- var ibnItemsArray = filteredItems.ToList();
+ var ibnItemsArray = filteredItems.ToList();
- IEnumerable<BaseItem> ibnItems = ibnItemsArray;
+ IEnumerable<BaseItem> ibnItems = ibnItemsArray;
- if (startIndex.HasValue || limit.HasValue)
+ if (startIndex.HasValue || limit.HasValue)
+ {
+ if (startIndex.HasValue)
{
- if (startIndex.HasValue)
- {
- ibnItems = ibnItems.Skip(startIndex.Value);
- }
-
- if (limit.HasValue)
- {
- ibnItems = ibnItems.Take(limit.Value);
- }
+ ibnItems = ibnItems.Skip(startIndex.Value);
}
- var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
-
- var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
-
- var result = new QueryResult<BaseItemDto>(
- startIndex,
- ibnItemsArray.Count,
- dtos.Where(i => i is not null).ToArray());
- return result;
- }
-
- /// <summary>
- /// Gets a year.
- /// </summary>
- /// <param name="year">The year.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Year returned.</response>
- /// <response code="404">Year not found.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the year,
- /// or a <see cref="NotFoundResult"/> if year not found.
- /// </returns>
- [HttpGet("{year}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId)
- {
- var item = _libraryManager.GetYear(year);
- if (item is null)
+ if (limit.HasValue)
{
- return NotFound();
+ ibnItems = ibnItems.Take(limit.Value);
}
+ }
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
+ var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
- if (userId.HasValue && !userId.Value.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
+
+ var result = new QueryResult<BaseItemDto>(
+ startIndex,
+ ibnItemsArray.Count,
+ dtos.Where(i => i is not null).ToArray());
+ return result;
+ }
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ /// <summary>
+ /// Gets a year.
+ /// </summary>
+ /// <param name="year">The year.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Year returned.</response>
+ /// <response code="404">Year not found.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the year,
+ /// or a <see cref="NotFoundResult"/> if year not found.
+ /// </returns>
+ [HttpGet("{year}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var item = _libraryManager.GetYear(year);
+ if (item is null)
+ {
+ return NotFound();
}
- private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes)
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
+
+ if (!userId.Value.Equals(default))
{
- var baseItemKind = f.GetBaseItemKind();
- // Exclude item types
- if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(baseItemKind))
- {
- return false;
- }
+ var user = _userManager.GetUserById(userId.Value);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- // Include item types
- if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind))
- {
- return false;
- }
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
+ }
- // Include MediaTypes
- if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
+ 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(baseItemKind))
+ {
+ return false;
+ }
- return true;
+ // Include item types
+ if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind))
+ {
+ return false;
}
- private IEnumerable<BaseItem> GetAllItems(IEnumerable<BaseItem> items)
+ // Include MediaTypes
+ if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
- return items
- .Select(i => i.ProductionYear ?? 0)
- .Where(i => i > 0)
- .Distinct()
- .Select(year => _libraryManager.GetYear(year));
+ return false;
}
+
+ return true;
+ }
+
+ private IEnumerable<BaseItem> GetAllItems(IEnumerable<BaseItem> items)
+ {
+ return items
+ .Select(i => i.ProductionYear ?? 0)
+ .Where(i => i > 0)
+ .Distinct()
+ .Select(year => _libraryManager.GetYear(year));
}
}
diff --git a/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs
index 6b3e78d4d..d2e8eb378 100644
--- a/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs
+++ b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs
@@ -71,8 +71,7 @@ public static class ClaimsPrincipalExtensions
public static bool GetIsApiKey(this ClaimsPrincipal user)
{
var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey);
- return !string.IsNullOrEmpty(claimValue)
- && bool.TryParse(claimValue, out var parsedClaimValue)
+ return bool.TryParse(claimValue, out var parsedClaimValue)
&& parsedClaimValue;
}
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index 9e784f7c4..2d7a56d91 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -5,112 +5,110 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
-using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Api.Extensions
+namespace Jellyfin.Api.Extensions;
+
+/// <summary>
+/// Dto Extensions.
+/// </summary>
+public static class DtoExtensions
{
/// <summary>
- /// Dto Extensions.
+ /// Add additional fields depending on client.
/// </summary>
- public static class DtoExtensions
+ /// <remarks>
+ /// Use in place of GetDtoOptions.
+ /// Legacy order: 2.
+ /// </remarks>
+ /// <param name="dtoOptions">DtoOptions object.</param>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>Modified DtoOptions object.</returns>
+ internal static DtoOptions AddClientFields(
+ this DtoOptions dtoOptions, ClaimsPrincipal user)
{
- /// <summary>
- /// Add additional fields depending on client.
- /// </summary>
- /// <remarks>
- /// Use in place of GetDtoOptions.
- /// Legacy order: 2.
- /// </remarks>
- /// <param name="dtoOptions">DtoOptions object.</param>
- /// <param name="user">Current claims principal.</param>
- /// <returns>Modified DtoOptions object.</returns>
- internal static DtoOptions AddClientFields(
- this DtoOptions dtoOptions, ClaimsPrincipal user)
- {
- dtoOptions.Fields ??= Array.Empty<ItemFields>();
+ dtoOptions.Fields ??= Array.Empty<ItemFields>();
- string? client = user.GetClient();
+ string? client = user.GetClient();
- // No client in claim
- if (string.IsNullOrEmpty(client))
- {
- return dtoOptions;
- }
+ // No client in claim
+ if (string.IsNullOrEmpty(client))
+ {
+ return dtoOptions;
+ }
- if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
+ if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
+ {
+ if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
{
- if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
- {
- int oldLen = dtoOptions.Fields.Count;
- var arr = new ItemFields[oldLen + 1];
- dtoOptions.Fields.CopyTo(arr, 0);
- arr[oldLen] = ItemFields.RecursiveItemCount;
- dtoOptions.Fields = arr;
- }
+ int oldLen = dtoOptions.Fields.Count;
+ var arr = new ItemFields[oldLen + 1];
+ dtoOptions.Fields.CopyTo(arr, 0);
+ arr[oldLen] = ItemFields.RecursiveItemCount;
+ dtoOptions.Fields = arr;
}
+ }
- if (!dtoOptions.ContainsField(ItemFields.ChildCount))
+ if (!dtoOptions.ContainsField(ItemFields.ChildCount))
+ {
+ if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
{
- if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
- {
- int oldLen = dtoOptions.Fields.Count;
- var arr = new ItemFields[oldLen + 1];
- dtoOptions.Fields.CopyTo(arr, 0);
- arr[oldLen] = ItemFields.ChildCount;
- dtoOptions.Fields = arr;
- }
+ int oldLen = dtoOptions.Fields.Count;
+ var arr = new ItemFields[oldLen + 1];
+ dtoOptions.Fields.CopyTo(arr, 0);
+ arr[oldLen] = ItemFields.ChildCount;
+ dtoOptions.Fields = arr;
}
-
- return dtoOptions;
}
- /// <summary>
- /// Add additional DtoOptions.
- /// </summary>
- /// <remarks>
- /// Converted from IHasDtoOptions.
- /// Legacy order: 3.
- /// </remarks>
- /// <param name="dtoOptions">DtoOptions object.</param>
- /// <param name="enableImages">Enable images.</param>
- /// <param name="enableUserData">Enable user data.</param>
- /// <param name="imageTypeLimit">Image type limit.</param>
- /// <param name="enableImageTypes">Enable image types.</param>
- /// <returns>Modified DtoOptions object.</returns>
- internal static DtoOptions AddAdditionalDtoOptions(
- this DtoOptions dtoOptions,
- bool? enableImages,
- bool? enableUserData,
- int? imageTypeLimit,
- IReadOnlyList<ImageType> enableImageTypes)
- {
- dtoOptions.EnableImages = enableImages ?? true;
+ return dtoOptions;
+ }
- if (imageTypeLimit.HasValue)
- {
- dtoOptions.ImageTypeLimit = imageTypeLimit.Value;
- }
+ /// <summary>
+ /// Add additional DtoOptions.
+ /// </summary>
+ /// <remarks>
+ /// Converted from IHasDtoOptions.
+ /// Legacy order: 3.
+ /// </remarks>
+ /// <param name="dtoOptions">DtoOptions object.</param>
+ /// <param name="enableImages">Enable images.</param>
+ /// <param name="enableUserData">Enable user data.</param>
+ /// <param name="imageTypeLimit">Image type limit.</param>
+ /// <param name="enableImageTypes">Enable image types.</param>
+ /// <returns>Modified DtoOptions object.</returns>
+ internal static DtoOptions AddAdditionalDtoOptions(
+ this DtoOptions dtoOptions,
+ bool? enableImages,
+ bool? enableUserData,
+ int? imageTypeLimit,
+ IReadOnlyList<ImageType> enableImageTypes)
+ {
+ dtoOptions.EnableImages = enableImages ?? true;
- if (enableUserData.HasValue)
- {
- dtoOptions.EnableUserData = enableUserData.Value;
- }
+ if (imageTypeLimit.HasValue)
+ {
+ dtoOptions.ImageTypeLimit = imageTypeLimit.Value;
+ }
- if (enableImageTypes.Count != 0)
- {
- dtoOptions.ImageTypes = enableImageTypes;
- }
+ if (enableUserData.HasValue)
+ {
+ dtoOptions.EnableUserData = enableUserData.Value;
+ }
- return dtoOptions;
+ if (enableImageTypes.Count != 0)
+ {
+ dtoOptions.ImageTypes = enableImageTypes;
}
+
+ return dtoOptions;
}
}
diff --git a/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs
new file mode 100644
index 000000000..96b29b1cb
--- /dev/null
+++ b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Extensions.Json;
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Formatters;
+
+/// <summary>
+/// Camel Case Json Profile Formatter.
+/// </summary>
+public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
+ /// </summary>
+ public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions)
+ {
+ SupportedMediaTypes.Clear();
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
+ }
+}
diff --git a/Jellyfin.Api/Formatters/CssOutputFormatter.cs b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
new file mode 100644
index 000000000..0a3891138
--- /dev/null
+++ b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
@@ -0,0 +1,35 @@
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Formatters;
+
+namespace Jellyfin.Api.Formatters;
+
+/// <summary>
+/// Css output formatter.
+/// </summary>
+public class CssOutputFormatter : TextOutputFormatter
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
+ /// </summary>
+ public CssOutputFormatter()
+ {
+ SupportedMediaTypes.Add("text/css");
+
+ SupportedEncodings.Add(Encoding.UTF8);
+ SupportedEncodings.Add(Encoding.Unicode);
+ }
+
+ /// <summary>
+ /// Write context object to stream.
+ /// </summary>
+ /// <param name="context">Writer context.</param>
+ /// <param name="selectedEncoding">Unused. Writer encoding.</param>
+ /// <returns>Write stream task.</returns>
+ public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
+ {
+ var stringResponse = context.Object?.ToString();
+ return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
+ }
+}
diff --git a/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs
new file mode 100644
index 000000000..b5b575278
--- /dev/null
+++ b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -0,0 +1,23 @@
+using System.Net.Mime;
+using Jellyfin.Extensions.Json;
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Formatters;
+
+/// <summary>
+/// Pascal Case Json Profile Formatter.
+/// </summary>
+public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
+ /// </summary>
+ public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions)
+ {
+ SupportedMediaTypes.Clear();
+ // Add application/json for default formatter
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json));
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType));
+ }
+}
diff --git a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
new file mode 100644
index 000000000..d5dea0f09
--- /dev/null
+++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
@@ -0,0 +1,32 @@
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Formatters;
+
+namespace Jellyfin.Api.Formatters;
+
+/// <summary>
+/// Xml output formatter.
+/// </summary>
+public class XmlOutputFormatter : TextOutputFormatter
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
+ /// </summary>
+ public XmlOutputFormatter()
+ {
+ SupportedMediaTypes.Clear();
+ SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
+
+ SupportedEncodings.Add(Encoding.UTF8);
+ SupportedEncodings.Add(Encoding.Unicode);
+ }
+
+ /// <inheritdoc />
+ public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
+ {
+ var stringResponse = context.Object?.ToString();
+ return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
+ }
+}
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index be410ebcd..2b18c389d 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -16,165 +16,164 @@ using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Audio helper.
+/// </summary>
+public class AudioHelper
{
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly EncodingHelper _encodingHelper;
+
/// <summary>
- /// Audio helper.
+ /// Initializes a new instance of the <see cref="AudioHelper"/> class.
/// </summary>
- public class AudioHelper
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+ /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ public AudioHelper(
+ IDlnaManager dlnaManager,
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ IHttpClientFactory httpClientFactory,
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
{
- private readonly IDlnaManager _dlnaManager;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceManager _deviceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IHttpContextAccessor _httpContextAccessor;
- private readonly EncodingHelper _encodingHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="AudioHelper"/> class.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
- /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- public AudioHelper(
- IDlnaManager dlnaManager,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- IHttpClientFactory httpClientFactory,
- IHttpContextAccessor httpContextAccessor,
- EncodingHelper encodingHelper)
+ _dlnaManager = dlnaManager;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _httpClientFactory = httpClientFactory;
+ _httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
+ }
+
+ /// <summary>
+ /// Get audio stream.
+ /// </summary>
+ /// <param name="transcodingJobType">Transcoding job type.</param>
+ /// <param name="streamingRequest">Streaming controller.Request dto.</param>
+ /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+ public async Task<ActionResult> GetAudioStream(
+ TranscodingJobType transcodingJobType,
+ StreamingRequestDto streamingRequest)
+ {
+ if (_httpContextAccessor.HttpContext is null)
{
- _dlnaManager = dlnaManager;
- _userManager = userManager;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _httpClientFactory = httpClientFactory;
- _httpContextAccessor = httpContextAccessor;
- _encodingHelper = encodingHelper;
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
}
- /// <summary>
- /// Get audio stream.
- /// </summary>
- /// <param name="transcodingJobType">Transcoding job type.</param>
- /// <param name="streamingRequest">Streaming controller.Request dto.</param>
- /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
- public async Task<ActionResult> GetAudioStream(
- TranscodingJobType transcodingJobType,
- StreamingRequestDto streamingRequest)
- {
- if (_httpContextAccessor.HttpContext is null)
- {
- throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
- }
+ bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
- bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
-
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
-
- using var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- _httpContextAccessor.HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- if (streamingRequest.Static && state.DirectStreamProvider is not null)
- {
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
- var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
- if (liveStreamInfo is null)
- {
- throw new FileNotFoundException();
- }
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ _httpContextAccessor.HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
- // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts"));
- }
+ if (streamingRequest.Static && state.DirectStreamProvider is not null)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
- // Static remote stream
- if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo is null)
{
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
-
- var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
+ throw new FileNotFoundException();
}
- if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
- {
- return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically");
- }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
+ // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts"));
+ }
- var outputPath = state.OutputFilePath;
- var outputPathExists = File.Exists(outputPath);
+ // Static remote stream
+ if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
- var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob is not null;
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
+ }
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+ if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
+ {
+ return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically");
+ }
- // Static stream
- if (streamingRequest.Static)
- {
- var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+ var outputPath = state.OutputFilePath;
+ var outputPathExists = File.Exists(outputPath);
- if (state.MediaSource.IsInfiniteStream)
- {
- var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
- return new FileStreamResult(stream, contentType);
- }
+ var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+ var isTranscodeCached = outputPathExists && transcodingJob is not null;
- return FileStreamResponseHelpers.GetStaticFileResult(
- state.MediaPath,
- contentType);
+ StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+
+ // Static stream
+ if (streamingRequest.Static)
+ {
+ var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+ if (state.MediaSource.IsInfiniteStream)
+ {
+ var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
- // Need to start ffmpeg (because media can't be returned directly)
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
- return await FileStreamResponseHelpers.GetTranscodedFile(
- state,
- isHeadRequest,
- _httpContextAccessor.HttpContext,
- _transcodingJobHelper,
- ffmpegCommandLineArguments,
- transcodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
+ return FileStreamResponseHelpers.GetStaticFileResult(
+ state.MediaPath,
+ contentType);
}
+
+ // Need to start ffmpeg (because media can't be returned directly)
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+ return await FileStreamResponseHelpers.GetTranscodedFile(
+ state,
+ isHeadRequest,
+ _httpContextAccessor.HttpContext,
+ _transcodingJobHelper,
+ ffmpegCommandLineArguments,
+ transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 010b181f7..245239233 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -24,722 +25,724 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Dynamic hls helper.
+/// </summary>
+public class DynamicHlsHelper
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly INetworkManager _networkManager;
+ private readonly ILogger<DynamicHlsHelper> _logger;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly EncodingHelper _encodingHelper;
+
/// <summary>
- /// Dynamic hls helper.
+ /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
/// </summary>
- public class DynamicHlsHelper
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
+ /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ public DynamicHlsHelper(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDlnaManager dlnaManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ INetworkManager networkManager,
+ ILogger<DynamicHlsHelper> logger,
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dlnaManager = dlnaManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _networkManager = networkManager;
+ _logger = logger;
+ _httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
+ }
+
+ /// <summary>
+ /// Get master hls playlist.
+ /// </summary>
+ /// <param name="transcodingJobType">Transcoding job type.</param>
+ /// <param name="streamingRequest">Streaming request dto.</param>
+ /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+ public async Task<ActionResult> GetMasterHlsPlaylist(
+ TranscodingJobType transcodingJobType,
+ StreamingRequestDto streamingRequest,
+ bool enableAdaptiveBitrateStreaming)
+ {
+ var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ return await GetMasterPlaylistInternal(
+ streamingRequest,
+ isHeadRequest,
+ enableAdaptiveBitrateStreaming,
+ transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+
+ private async Task<ActionResult> GetMasterPlaylistInternal(
+ StreamingRequestDto streamingRequest,
+ bool isHeadRequest,
+ bool enableAdaptiveBitrateStreaming,
+ TranscodingJobType transcodingJobType,
+ CancellationTokenSource cancellationTokenSource)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceManager _deviceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly INetworkManager _networkManager;
- private readonly ILogger<DynamicHlsHelper> _logger;
- private readonly IHttpContextAccessor _httpContextAccessor;
- private readonly EncodingHelper _encodingHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- public DynamicHlsHelper(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDlnaManager dlnaManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- INetworkManager networkManager,
- ILogger<DynamicHlsHelper> logger,
- IHttpContextAccessor httpContextAccessor,
- EncodingHelper encodingHelper)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dlnaManager = dlnaManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _networkManager = networkManager;
- _logger = logger;
- _httpContextAccessor = httpContextAccessor;
- _encodingHelper = encodingHelper;
- }
-
- /// <summary>
- /// Get master hls playlist.
- /// </summary>
- /// <param name="transcodingJobType">Transcoding job type.</param>
- /// <param name="streamingRequest">Streaming request dto.</param>
- /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
- /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
- public async Task<ActionResult> GetMasterHlsPlaylist(
- TranscodingJobType transcodingJobType,
- StreamingRequestDto streamingRequest,
- bool enableAdaptiveBitrateStreaming)
- {
- var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- return await GetMasterPlaylistInternal(
+ if (_httpContextAccessor.HttpContext is null)
+ {
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+ }
+
+ using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
- isHeadRequest,
- enableAdaptiveBitrateStreaming,
+ _httpContextAccessor.HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
transcodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
- }
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
- private async Task<ActionResult> GetMasterPlaylistInternal(
- StreamingRequestDto streamingRequest,
- bool isHeadRequest,
- bool enableAdaptiveBitrateStreaming,
- TranscodingJobType transcodingJobType,
- CancellationTokenSource cancellationTokenSource)
+ _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
+ if (isHeadRequest)
{
- if (_httpContextAccessor.HttpContext is null)
- {
- throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
- }
+ return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- using var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- _httpContextAccessor.HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
- if (isHeadRequest)
- {
- return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
- }
+ var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
- var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
+ var builder = new StringBuilder();
- var builder = new StringBuilder();
+ builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXTM3U");
+ var isLiveStream = state.IsSegmentedLiveStream;
- var isLiveStream = state.IsSegmentedLiveStream;
+ var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
- var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
+ // from universal audio service
+ if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
+ && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
+ {
+ queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
+ }
- // from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
- && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
- {
- queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
- }
+ // from universal audio service
+ if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
+ && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
+ {
+ queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
+ }
- // from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
- && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
- {
- queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
- }
+ // Main stream
+ var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
- // Main stream
- var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
+ playlistUrl += queryString;
- playlistUrl += queryString;
+ var subtitleStreams = state.MediaSource
+ .MediaStreams
+ .Where(i => i.IsTextSubtitleStream)
+ .ToList();
- var subtitleStreams = state.MediaSource
- .MediaStreams
- .Where(i => i.IsTextSubtitleStream)
- .ToList();
+ var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
+ ? "subs"
+ : null;
- var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
- ? "subs"
- : null;
+ // If we're burning in subtitles then don't add additional subs to the manifest
+ if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ subtitleGroup = null;
+ }
- // If we're burning in subtitles then don't add additional subs to the manifest
- if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
- {
- subtitleGroup = null;
- }
+ if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ {
+ AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
+ }
+
+ var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ if (state.VideoStream is not null && state.VideoRequest is not null)
+ {
+ // Provide a workaround for the case issue between flac and fLaC.
+ var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
+ if (!string.IsNullOrEmpty(flacWaPlaylist))
{
- AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
+ builder.Append(flacWaPlaylist);
}
- var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- if (state.VideoStream is not null && state.VideoRequest is not null)
+ // Provide SDR HEVC entrance for backward compatibility.
+ if (encodingOptions.AllowHevcEncoding
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+ && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- // Provide a workaround for the case issue between flac and fLaC.
- var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
-
- // Provide SDR HEVC entrance for backward compatibility.
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
- && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
- if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
- {
- // Force HEVC Main Profile and disable video stream copy.
- state.OutputVideoCodec = "hevc";
- var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
- sdrVideoUrl += "&AllowVideoStreamCopy=false";
-
- var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
- var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
- var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
-
- var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
-
- // Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
-
- // Restore the video codec
- state.OutputVideoCodec = "copy";
- }
- }
-
- // Provide Level 5.0 entrance for backward compatibility.
- // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
- // but in fact it is capable of playing videos up to Level 6.1.
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream.Level.HasValue
- && state.VideoStream.Level > 150
- && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
- && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
+ if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
{
- var playlistCodecsField = new StringBuilder();
- AppendPlaylistCodecsField(playlistCodecsField, state);
+ // Force HEVC Main Profile and disable video stream copy.
+ state.OutputVideoCodec = "hevc";
+ var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
+ sdrVideoUrl += "&AllowVideoStreamCopy=false";
- // Force the video level to 5.0.
- var originalLevel = state.VideoStream.Level;
- state.VideoStream.Level = 150;
- var newPlaylistCodecsField = new StringBuilder();
- AppendPlaylistCodecsField(newPlaylistCodecsField, state);
+ var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
+ var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
+ var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
- // Restore the video level.
- state.VideoStream.Level = originalLevel;
- var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
- builder.Append(newPlaylist);
+ var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
+ flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
if (!string.IsNullOrEmpty(flacWaPlaylist))
{
builder.Append(flacWaPlaylist);
}
+
+ // Restore the video codec
+ state.OutputVideoCodec = "copy";
}
}
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
- {
- var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
-
- // By default, vary by just 200k
- var variation = GetBitrateVariation(totalBitrate);
-
- var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+ // Provide Level 5.0 entrance for backward compatibility.
+ // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
+ // but in fact it is capable of playing videos up to Level 6.1.
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream.Level.HasValue
+ && state.VideoStream.Level > 150
+ && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+ && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ var playlistCodecsField = new StringBuilder();
+ AppendPlaylistCodecsField(playlistCodecsField, state);
+
+ // Force the video level to 5.0.
+ var originalLevel = state.VideoStream.Level;
+ state.VideoStream.Level = 150;
+ var newPlaylistCodecsField = new StringBuilder();
+ AppendPlaylistCodecsField(newPlaylistCodecsField, state);
+
+ // Restore the video level.
+ state.VideoStream.Level = originalLevel;
+ var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
+ builder.Append(newPlaylist);
- variation *= 2;
- newBitrate = totalBitrate - variation;
- variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+ // Provide a workaround for the case issue between flac and fLaC.
+ flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
+ if (!string.IsNullOrEmpty(flacWaPlaylist))
+ {
+ builder.Append(flacWaPlaylist);
+ }
}
-
- return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
- private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+ if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
{
- var playlistBuilder = new StringBuilder();
- playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture))
- .Append(",AVERAGE-BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture));
+ var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
- AppendPlaylistVideoRangeField(playlistBuilder, state);
+ // By default, vary by just 200k
+ var variation = GetBitrateVariation(totalBitrate);
- AppendPlaylistCodecsField(playlistBuilder, state);
+ var newBitrate = totalBitrate - variation;
+ var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
- AppendPlaylistResolutionField(playlistBuilder, state);
+ variation *= 2;
+ newBitrate = totalBitrate - variation;
+ variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+ }
- AppendPlaylistFramerateField(playlistBuilder, state);
+ return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- playlistBuilder.Append(",SUBTITLES=\"")
- .Append(subtitleGroup)
- .Append('"');
- }
+ private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+ {
+ var playlistBuilder = new StringBuilder();
+ playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture))
+ .Append(",AVERAGE-BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture));
+
+ AppendPlaylistVideoRangeField(playlistBuilder, state);
- playlistBuilder.Append(Environment.NewLine);
- playlistBuilder.AppendLine(url);
- builder.Append(playlistBuilder);
+ AppendPlaylistCodecsField(playlistBuilder, state);
- return playlistBuilder;
+ AppendPlaylistResolutionField(playlistBuilder, state);
+
+ AppendPlaylistFramerateField(playlistBuilder, state);
+
+ if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ {
+ playlistBuilder.Append(",SUBTITLES=\"")
+ .Append(subtitleGroup)
+ .Append('"');
}
- /// <summary>
- /// Appends a VIDEO-RANGE field containing the range of the output video stream.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
+ playlistBuilder.Append(Environment.NewLine);
+ playlistBuilder.AppendLine(url);
+ builder.Append(playlistBuilder);
+
+ return playlistBuilder;
+ }
+
+ /// <summary>
+ /// Appends a VIDEO-RANGE field containing the range of the output video stream.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
+ {
+ if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
{
- if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
+ var videoRange = state.VideoStream.VideoRange;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
- var videoRange = state.VideoStream.VideoRange;
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
- {
- builder.Append(",VIDEO-RANGE=SDR");
- }
-
- if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
- {
- builder.Append(",VIDEO-RANGE=PQ");
- }
- }
- else
+ if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
{
- // Currently we only encode to SDR.
builder.Append(",VIDEO-RANGE=SDR");
}
- }
- }
- /// <summary>
- /// Appends a CODECS field containing formatted strings of
- /// the active streams output video and audio codecs.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
- {
- // Video
- string videoCodecs = string.Empty;
- int? videoCodecLevel = GetOutputVideoCodecLevel(state);
- if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
- {
- videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+ if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append(",VIDEO-RANGE=PQ");
+ }
}
-
- // Audio
- string audioCodecs = string.Empty;
- if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+ else
{
- audioCodecs = GetPlaylistAudioCodecs(state);
+ // Currently we only encode to SDR.
+ builder.Append(",VIDEO-RANGE=SDR");
}
+ }
+ }
- StringBuilder codecs = new StringBuilder();
+ /// <summary>
+ /// Appends a CODECS field containing formatted strings of
+ /// the active streams output video and audio codecs.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+ /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
+ {
+ // Video
+ string videoCodecs = string.Empty;
+ int? videoCodecLevel = GetOutputVideoCodecLevel(state);
+ if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
+ {
+ videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+ }
- codecs.Append(videoCodecs);
+ // Audio
+ string audioCodecs = string.Empty;
+ if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+ {
+ audioCodecs = GetPlaylistAudioCodecs(state);
+ }
- if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
- {
- codecs.Append(',');
- }
+ StringBuilder codecs = new StringBuilder();
- codecs.Append(audioCodecs);
+ codecs.Append(videoCodecs);
- if (codecs.Length > 1)
- {
- builder.Append(",CODECS=\"")
- .Append(codecs)
- .Append('"');
- }
+ if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
+ {
+ codecs.Append(',');
}
- /// <summary>
- /// Appends a RESOLUTION field containing the resolution of the output stream.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
+ codecs.Append(audioCodecs);
+
+ if (codecs.Length > 1)
{
- if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
- {
- builder.Append(",RESOLUTION=")
- .Append(state.OutputWidth.GetValueOrDefault())
- .Append('x')
- .Append(state.OutputHeight.GetValueOrDefault());
- }
+ builder.Append(",CODECS=\"")
+ .Append(codecs)
+ .Append('"');
}
+ }
- /// <summary>
- /// Appends a FRAME-RATE field containing the framerate of the output stream.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
+ /// <summary>
+ /// Appends a RESOLUTION field containing the resolution of the output stream.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
+ {
+ if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
{
- double? framerate = null;
- if (state.TargetFramerate.HasValue)
- {
- framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
- }
- else if (state.VideoStream?.RealFrameRate is not null)
- {
- framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
- }
+ builder.Append(",RESOLUTION=")
+ .Append(state.OutputWidth.GetValueOrDefault())
+ .Append('x')
+ .Append(state.OutputHeight.GetValueOrDefault());
+ }
+ }
- if (framerate.HasValue)
- {
- builder.Append(",FRAME-RATE=")
- .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
- }
+ /// <summary>
+ /// Appends a FRAME-RATE field containing the framerate of the output stream.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
+ {
+ double? framerate = null;
+ if (state.TargetFramerate.HasValue)
+ {
+ framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
+ }
+ else if (state.VideoStream?.RealFrameRate is not null)
+ {
+ framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
}
- private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
+ if (framerate.HasValue)
{
- // Within the local network this will likely do more harm than good.
- if (_networkManager.IsInLocalNetwork(ipAddress))
- {
- return false;
- }
+ builder.Append(",FRAME-RATE=")
+ .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ }
- if (!enableAdaptiveBitrateStreaming)
- {
- return false;
- }
+ private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
+ {
+ // Within the local network this will likely do more harm than good.
+ if (_networkManager.IsInLocalNetwork(ipAddress))
+ {
+ return false;
+ }
- if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
- {
- // Opening live streams is so slow it's not even worth it
- return false;
- }
+ if (!enableAdaptiveBitrateStreaming)
+ {
+ return false;
+ }
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- return false;
- }
+ if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
+ {
+ // Opening live streams is so slow it's not even worth it
+ return false;
+ }
- if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
- {
- return false;
- }
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ return false;
+ }
- if (!state.IsOutputVideo)
- {
- return false;
- }
+ if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
+ {
+ return false;
+ }
- // Having problems in android
+ if (!state.IsOutputVideo)
+ {
return false;
- // return state.VideoRequest.VideoBitRate.HasValue;
}
- private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
+ // Having problems in android
+ return false;
+ // return state.VideoRequest.VideoBitRate.HasValue;
+ }
+
+ private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
+ {
+ if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
{
- if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
- {
- return;
- }
+ return;
+ }
- var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
- const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
+ var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
+ const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
- foreach (var stream in subtitles)
- {
- var name = stream.DisplayTitle;
-
- var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
- var isForced = stream.IsForced;
-
- var url = string.Format(
- CultureInfo.InvariantCulture,
- "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
- state.Request.MediaSourceId,
- stream.Index.ToString(CultureInfo.InvariantCulture),
- 30.ToString(CultureInfo.InvariantCulture),
- user.GetToken());
-
- var line = string.Format(
- CultureInfo.InvariantCulture,
- Format,
- name,
- isDefault ? "YES" : "NO",
- isForced ? "YES" : "NO",
- url,
- stream.Language ?? "Unknown");
-
- builder.AppendLine(line);
- }
+ foreach (var stream in subtitles)
+ {
+ var name = stream.DisplayTitle;
+
+ var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
+ var isForced = stream.IsForced;
+
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
+ state.Request.MediaSourceId,
+ stream.Index.ToString(CultureInfo.InvariantCulture),
+ 30.ToString(CultureInfo.InvariantCulture),
+ user.GetToken());
+
+ var line = string.Format(
+ CultureInfo.InvariantCulture,
+ Format,
+ name,
+ isDefault ? "YES" : "NO",
+ isForced ? "YES" : "NO",
+ url,
+ stream.Language ?? "Unknown");
+
+ builder.AppendLine(line);
}
+ }
- /// <summary>
- /// Get the H.26X level of the output video stream.
- /// </summary>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>H.26X level of the output video stream.</returns>
- private int? GetOutputVideoCodecLevel(StreamState state)
+ /// <summary>
+ /// Get the H.26X level of the output video stream.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <returns>H.26X level of the output video stream.</returns>
+ private int? GetOutputVideoCodecLevel(StreamState state)
+ {
+ string levelString = string.Empty;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream is not null
+ && state.VideoStream.Level.HasValue)
{
- string levelString = string.Empty;
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream is not null
- && state.VideoStream.Level.HasValue)
+ levelString = state.VideoStream.Level.ToString() ?? string.Empty;
+ }
+ else
+ {
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
- levelString = state.VideoStream.Level.ToString() ?? string.Empty;
+ levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
}
- else
- {
- if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
- levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
- }
- if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
- levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
- }
- }
-
- if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return parsedLevel;
+ levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
}
-
- return null;
}
- /// <summary>
- /// Get the H.26X profile of the output video stream.
- /// </summary>
- /// <param name="state">StreamState of the current stream.</param>
- /// <param name="codec">Video codec.</param>
- /// <returns>H.26X profile of the output video stream.</returns>
- private string GetOutputVideoCodecProfile(StreamState state, string codec)
+ if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
{
- string profileString = string.Empty;
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && !string.IsNullOrEmpty(state.VideoStream.Profile))
- {
- profileString = state.VideoStream.Profile;
- }
- else if (!string.IsNullOrEmpty(codec))
- {
- profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
- if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- profileString ??= "high";
- }
+ return parsedLevel;
+ }
- if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- profileString ??= "main";
- }
- }
+ return null;
+ }
- return profileString;
+ /// <summary>
+ /// Get the H.26X profile of the output video stream.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <param name="codec">Video codec.</param>
+ /// <returns>H.26X profile of the output video stream.</returns>
+ private string GetOutputVideoCodecProfile(StreamState state, string codec)
+ {
+ string profileString = string.Empty;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrEmpty(state.VideoStream.Profile))
+ {
+ profileString = state.VideoStream.Profile;
}
-
- /// <summary>
- /// Gets a formatted string of the output audio codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>Formatted audio codec string.</returns>
- private string GetPlaylistAudioCodecs(StreamState state)
+ else if (!string.IsNullOrEmpty(codec))
{
- if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+ profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
- string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
- return HlsCodecStringHelpers.GetAACString(profile);
+ profileString ??= "high";
}
- if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return HlsCodecStringHelpers.GetMP3String();
+ profileString ??= "main";
}
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetAC3String();
- }
+ return profileString;
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetEAC3String();
- }
+ /// <summary>
+ /// Gets a formatted string of the output audio codec, for use in the CODECS field.
+ /// </summary>
+ /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+ /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <returns>Formatted audio codec string.</returns>
+ private string GetPlaylistAudioCodecs(StreamState state)
+ {
+ if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+ {
+ string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
+ return HlsCodecStringHelpers.GetAACString(profile);
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetFLACString();
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetMP3String();
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetALACString();
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetAC3String();
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetOPUSString();
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetEAC3String();
+ }
- return string.Empty;
+ if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetFLACString();
}
- /// <summary>
- /// Gets a formatted string of the output video codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <param name="codec">Video codec.</param>
- /// <param name="level">Video level.</param>
- /// <returns>Formatted video codec string.</returns>
- private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
+ if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
{
- if (level == 0)
- {
- // This is 0 when there's no requested H.26X level in the device profile
- // and the source is not encoded in H.26X
- _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
- return string.Empty;
- }
+ return HlsCodecStringHelpers.GetALACString();
+ }
- if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- string profile = GetOutputVideoCodecProfile(state, "h264");
- return HlsCodecStringHelpers.GetH264String(profile, level);
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetOPUSString();
+ }
- if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- string profile = GetOutputVideoCodecProfile(state, "hevc");
- return HlsCodecStringHelpers.GetH265String(profile, level);
- }
+ return string.Empty;
+ }
+ /// <summary>
+ /// Gets a formatted string of the output video codec, for use in the CODECS field.
+ /// </summary>
+ /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+ /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <param name="codec">Video codec.</param>
+ /// <param name="level">Video level.</param>
+ /// <returns>Formatted video codec string.</returns>
+ private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
+ {
+ if (level == 0)
+ {
+ // This is 0 when there's no requested H.26X level in the device profile
+ // and the source is not encoded in H.26X
+ _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
return string.Empty;
}
- private int GetBitrateVariation(int bitrate)
+ if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{
- // By default, vary by just 50k
- var variation = 50000;
-
- if (bitrate >= 10000000)
- {
- variation = 2000000;
- }
- else if (bitrate >= 5000000)
- {
- variation = 1500000;
- }
- else if (bitrate >= 3000000)
- {
- variation = 1000000;
- }
- else if (bitrate >= 2000000)
- {
- variation = 500000;
- }
- else if (bitrate >= 1000000)
- {
- variation = 300000;
- }
- else if (bitrate >= 600000)
- {
- variation = 200000;
- }
- else if (bitrate >= 400000)
- {
- variation = 100000;
- }
-
- return variation;
+ string profile = GetOutputVideoCodecProfile(state, "h264");
+ return HlsCodecStringHelpers.GetH264String(profile, level);
}
- private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
+ if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return url.Replace(
- "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
- "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
- StringComparison.OrdinalIgnoreCase);
+ string profile = GetOutputVideoCodecProfile(state, "hevc");
+ return HlsCodecStringHelpers.GetH265String(profile, level);
}
- private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
+ return string.Empty;
+ }
+
+ private int GetBitrateVariation(int bitrate)
+ {
+ // By default, vary by just 50k
+ var variation = 50000;
+
+ if (bitrate >= 10000000)
{
- string profileStr = codec + "-profile=";
- return url.Replace(
- profileStr + oldValue,
- profileStr + newValue,
- StringComparison.OrdinalIgnoreCase);
+ variation = 2000000;
}
-
- private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
+ else if (bitrate >= 5000000)
{
- var oldPlaylist = playlist.ToString();
- return oldPlaylist.Replace(
- oldValue.ToString(),
- newValue.ToString(),
- StringComparison.Ordinal);
+ variation = 1500000;
}
-
- private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
+ else if (bitrate >= 3000000)
{
- if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
- {
- return string.Empty;
- }
+ variation = 1000000;
+ }
+ else if (bitrate >= 2000000)
+ {
+ variation = 500000;
+ }
+ else if (bitrate >= 1000000)
+ {
+ variation = 300000;
+ }
+ else if (bitrate >= 600000)
+ {
+ variation = 200000;
+ }
+ else if (bitrate >= 400000)
+ {
+ variation = 100000;
+ }
- var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
+ return variation;
+ }
+
+ private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
+ {
+ return url.Replace(
+ "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
+ "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
+ {
+ string profileStr = codec + "-profile=";
+ return url.Replace(
+ profileStr + oldValue,
+ profileStr + newValue,
+ StringComparison.OrdinalIgnoreCase);
+ }
- return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
+ private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
+ {
+ var oldPlaylist = playlist.ToString();
+ return oldPlaylist.Replace(
+ oldValue.ToString(),
+ newValue.ToString(),
+ StringComparison.Ordinal);
+ }
+
+ private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
+ {
+ if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Empty;
}
+
+ var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
+
+ return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
}
}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 5bdd3fe2e..0f0a70c69 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -11,110 +11,109 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// The stream response helpers.
+/// </summary>
+public static class FileStreamResponseHelpers
{
/// <summary>
- /// The stream response helpers.
+ /// Returns a static file from a remote source.
/// </summary>
- public static class FileStreamResponseHelpers
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
+ /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
+ /// <param name="httpContext">The current http context.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
+ /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
+ public static async Task<ActionResult> GetStaticRemoteStreamResult(
+ StreamState state,
+ HttpClient httpClient,
+ HttpContext httpContext,
+ CancellationToken cancellationToken = default)
{
- /// <summary>
- /// Returns a static file from a remote source.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
- /// <param name="httpContext">The current http context.</param>
- /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
- /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
- public static async Task<ActionResult> GetStaticRemoteStreamResult(
- StreamState state,
- HttpClient httpClient,
- HttpContext httpContext,
- CancellationToken cancellationToken = default)
+ if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
{
- if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
- {
- httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
- }
+ httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
+ }
- // Can't dispose the response as it's required up the call chain.
- var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
+ // Can't dispose the response as it's required up the call chain.
+ var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
+ var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
- httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
+ httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
- return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
- }
+ return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
+ }
- /// <summary>
- /// Returns a static file from the server.
- /// </summary>
- /// <param name="path">The path to the file.</param>
- /// <param name="contentType">The content type of the file.</param>
- /// <returns>An <see cref="ActionResult"/> the file.</returns>
- public static ActionResult GetStaticFileResult(
- string path,
- string contentType)
- {
- return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
- }
+ /// <summary>
+ /// Returns a static file from the server.
+ /// </summary>
+ /// <param name="path">The path to the file.</param>
+ /// <param name="contentType">The content type of the file.</param>
+ /// <returns>An <see cref="ActionResult"/> the file.</returns>
+ public static ActionResult GetStaticFileResult(
+ string path,
+ string contentType)
+ {
+ return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
+ }
- /// <summary>
- /// Returns a transcoded file from the server.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
- /// <param name="httpContext">The current http context.</param>
- /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
- /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
- /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
- /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
- /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
- public static async Task<ActionResult> GetTranscodedFile(
- StreamState state,
- bool isHeadRequest,
- HttpContext httpContext,
- TranscodingJobHelper transcodingJobHelper,
- string ffmpegCommandLineArguments,
- TranscodingJobType transcodingJobType,
- CancellationTokenSource cancellationTokenSource)
- {
- // Use the command line args with a dummy playlist path
- var outputPath = state.OutputFilePath;
+ /// <summary>
+ /// Returns a transcoded file from the server.
+ /// </summary>
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
+ /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
+ /// <param name="httpContext">The current http context.</param>
+ /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
+ /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
+ /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+ /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
+ /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
+ public static async Task<ActionResult> GetTranscodedFile(
+ StreamState state,
+ bool isHeadRequest,
+ HttpContext httpContext,
+ TranscodingJobHelper transcodingJobHelper,
+ string ffmpegCommandLineArguments,
+ TranscodingJobType transcodingJobType,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ // Use the command line args with a dummy playlist path
+ var outputPath = state.OutputFilePath;
- httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
+ httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
- var contentType = state.GetMimeType(outputPath);
+ var contentType = state.GetMimeType(outputPath);
- // Headers only
- if (isHeadRequest)
- {
- httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
- return new OkResult();
- }
+ // Headers only
+ if (isHeadRequest)
+ {
+ httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
+ return new OkResult();
+ }
- var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- try
+ var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ try
+ {
+ TranscodingJobDto? job;
+ if (!File.Exists(outputPath))
{
- TranscodingJobDto? job;
- if (!File.Exists(outputPath))
- {
- job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
- }
- else
- {
- job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
- state.Dispose();
- }
-
- var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
- return new FileStreamResult(stream, contentType);
+ job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
}
- finally
+ else
{
- transcodingLock.Release();
+ job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+ state.Dispose();
}
+
+ var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
+ }
+ finally
+ {
+ transcodingLock.Release();
}
}
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index cbe82979b..995488397 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -2,182 +2,181 @@
using System.Globalization;
using System.Text;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Hls Codec string helpers.
+/// </summary>
+public static class HlsCodecStringHelpers
{
/// <summary>
- /// Hls Codec string helpers.
+ /// Codec name for MP3.
+ /// </summary>
+ public const string MP3 = "mp4a.40.34";
+
+ /// <summary>
+ /// Codec name for AC-3.
+ /// </summary>
+ public const string AC3 = "mp4a.a5";
+
+ /// <summary>
+ /// Codec name for E-AC-3.
+ /// </summary>
+ public const string EAC3 = "mp4a.a6";
+
+ /// <summary>
+ /// Codec name for FLAC.
+ /// </summary>
+ public const string FLAC = "flac";
+
+ /// <summary>
+ /// Codec name for ALAC.
+ /// </summary>
+ public const string ALAC = "alac";
+
+ /// <summary>
+ /// Codec name for OPUS.
+ /// </summary>
+ public const string OPUS = "opus";
+
+ /// <summary>
+ /// Gets a MP3 codec string.
/// </summary>
- public static class HlsCodecStringHelpers
+ /// <returns>MP3 codec string.</returns>
+ public static string GetMP3String()
{
- /// <summary>
- /// Codec name for MP3.
- /// </summary>
- public const string MP3 = "mp4a.40.34";
-
- /// <summary>
- /// Codec name for AC-3.
- /// </summary>
- public const string AC3 = "mp4a.a5";
-
- /// <summary>
- /// Codec name for E-AC-3.
- /// </summary>
- public const string EAC3 = "mp4a.a6";
-
- /// <summary>
- /// Codec name for FLAC.
- /// </summary>
- public const string FLAC = "flac";
-
- /// <summary>
- /// Codec name for ALAC.
- /// </summary>
- public const string ALAC = "alac";
-
- /// <summary>
- /// Codec name for OPUS.
- /// </summary>
- public const string OPUS = "opus";
-
- /// <summary>
- /// Gets a MP3 codec string.
- /// </summary>
- /// <returns>MP3 codec string.</returns>
- public static string GetMP3String()
- {
- return MP3;
- }
+ return MP3;
+ }
- /// <summary>
- /// Gets an AAC codec string.
- /// </summary>
- /// <param name="profile">AAC profile.</param>
- /// <returns>AAC codec string.</returns>
- public static string GetAACString(string? profile)
+ /// <summary>
+ /// Gets an AAC codec string.
+ /// </summary>
+ /// <param name="profile">AAC profile.</param>
+ /// <returns>AAC codec string.</returns>
+ public static string GetAACString(string? profile)
+ {
+ StringBuilder result = new StringBuilder("mp4a", 9);
+
+ if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
{
- StringBuilder result = new StringBuilder("mp4a", 9);
-
- if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".40.5");
- }
- else
- {
- // Default to LC if profile is invalid
- result.Append(".40.2");
- }
-
- return result.ToString();
+ result.Append(".40.5");
}
-
- /// <summary>
- /// Gets an AC-3 codec string.
- /// </summary>
- /// <returns>AC-3 codec string.</returns>
- public static string GetAC3String()
+ else
{
- return AC3;
+ // Default to LC if profile is invalid
+ result.Append(".40.2");
}
- /// <summary>
- /// Gets an E-AC-3 codec string.
- /// </summary>
- /// <returns>E-AC-3 codec string.</returns>
- public static string GetEAC3String()
+ return result.ToString();
+ }
+
+ /// <summary>
+ /// Gets an AC-3 codec string.
+ /// </summary>
+ /// <returns>AC-3 codec string.</returns>
+ public static string GetAC3String()
+ {
+ return AC3;
+ }
+
+ /// <summary>
+ /// Gets an E-AC-3 codec string.
+ /// </summary>
+ /// <returns>E-AC-3 codec string.</returns>
+ public static string GetEAC3String()
+ {
+ return EAC3;
+ }
+
+ /// <summary>
+ /// Gets an FLAC codec string.
+ /// </summary>
+ /// <returns>FLAC codec string.</returns>
+ public static string GetFLACString()
+ {
+ return FLAC;
+ }
+
+ /// <summary>
+ /// Gets an ALAC codec string.
+ /// </summary>
+ /// <returns>ALAC codec string.</returns>
+ public static string GetALACString()
+ {
+ return ALAC;
+ }
+
+ /// <summary>
+ /// Gets an OPUS codec string.
+ /// </summary>
+ /// <returns>OPUS codec string.</returns>
+ public static string GetOPUSString()
+ {
+ return OPUS;
+ }
+
+ /// <summary>
+ /// Gets a H.264 codec string.
+ /// </summary>
+ /// <param name="profile">H.264 profile.</param>
+ /// <param name="level">H.264 level.</param>
+ /// <returns>H.264 string.</returns>
+ public static string GetH264String(string? profile, int level)
+ {
+ StringBuilder result = new StringBuilder("avc1", 11);
+
+ if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
{
- return EAC3;
+ result.Append(".6400");
}
-
- /// <summary>
- /// Gets an FLAC codec string.
- /// </summary>
- /// <returns>FLAC codec string.</returns>
- public static string GetFLACString()
+ else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
{
- return FLAC;
+ result.Append(".4D40");
}
-
- /// <summary>
- /// Gets an ALAC codec string.
- /// </summary>
- /// <returns>ALAC codec string.</returns>
- public static string GetALACString()
+ else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
{
- return ALAC;
+ result.Append(".42E0");
}
-
- /// <summary>
- /// Gets an OPUS codec string.
- /// </summary>
- /// <returns>OPUS codec string.</returns>
- public static string GetOPUSString()
+ else
{
- return OPUS;
+ // Default to constrained baseline if profile is invalid
+ result.Append(".4240");
}
- /// <summary>
- /// Gets a H.264 codec string.
- /// </summary>
- /// <param name="profile">H.264 profile.</param>
- /// <param name="level">H.264 level.</param>
- /// <returns>H.264 string.</returns>
- public static string GetH264String(string? profile, int level)
+ string levelHex = level.ToString("X2", CultureInfo.InvariantCulture);
+ result.Append(levelHex);
+
+ return result.ToString();
+ }
+
+ /// <summary>
+ /// Gets a H.265 codec string.
+ /// </summary>
+ /// <param name="profile">H.265 profile.</param>
+ /// <param name="level">H.265 level.</param>
+ /// <returns>H.265 string.</returns>
+ public static string GetH265String(string? profile, int level)
+ {
+ // The h265 syntax is a bit of a mystery at the time this comment was written.
+ // This is what I've found through various sources:
+ // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
+ StringBuilder result = new StringBuilder("hvc1", 16);
+
+ if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
{
- StringBuilder result = new StringBuilder("avc1", 11);
-
- if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".6400");
- }
- else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".4D40");
- }
- else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".42E0");
- }
- else
- {
- // Default to constrained baseline if profile is invalid
- result.Append(".4240");
- }
-
- string levelHex = level.ToString("X2", CultureInfo.InvariantCulture);
- result.Append(levelHex);
-
- return result.ToString();
+ result.Append(".2.4");
}
-
- /// <summary>
- /// Gets a H.265 codec string.
- /// </summary>
- /// <param name="profile">H.265 profile.</param>
- /// <param name="level">H.265 level.</param>
- /// <returns>H.265 string.</returns>
- public static string GetH265String(string? profile, int level)
+ else
{
- // The h265 syntax is a bit of a mystery at the time this comment was written.
- // This is what I've found through various sources:
- // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
- StringBuilder result = new StringBuilder("hvc1", 16);
-
- if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
- || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".2.4");
- }
- else
- {
- // Default to main if profile is invalid
- result.Append(".1.4");
- }
-
- result.Append(".L")
- .Append(level)
- .Append(".B0");
-
- return result.ToString();
+ // Default to main if profile is invalid
+ result.Append(".1.4");
}
+
+ result.Append(".L")
+ .Append(level)
+ .Append(".B0");
+
+ return result.ToString();
}
}
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index 671107c1f..2155e305d 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -8,131 +8,130 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// The hls helpers.
+/// </summary>
+public static class HlsHelpers
{
/// <summary>
- /// The hls helpers.
+ /// Waits for a minimum number of segments to be available.
/// </summary>
- public static class HlsHelpers
+ /// <param name="playlist">The playlist string.</param>
+ /// <param name="segmentCount">The segment count.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+ /// <returns>A <see cref="Task"/> indicating the waiting process.</returns>
+ public static async Task WaitForMinimumSegmentCount(string playlist, int? segmentCount, ILogger logger, CancellationToken cancellationToken)
{
- /// <summary>
- /// Waits for a minimum number of segments to be available.
- /// </summary>
- /// <param name="playlist">The playlist string.</param>
- /// <param name="segmentCount">The segment count.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
- /// <returns>A <see cref="Task"/> indicating the waiting process.</returns>
- public static async Task WaitForMinimumSegmentCount(string playlist, int? segmentCount, ILogger logger, CancellationToken cancellationToken)
- {
- logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
+ logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
- while (!cancellationToken.IsCancellationRequested)
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
{
- try
+ // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
+ var fileStream = new FileStream(
+ playlist,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite,
+ IODefaults.FileStreamBufferSize,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
+ await using (fileStream.ConfigureAwait(false))
{
- // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
- var fileStream = new FileStream(
- playlist,
- FileMode.Open,
- FileAccess.Read,
- FileShare.ReadWrite,
- IODefaults.FileStreamBufferSize,
- FileOptions.Asynchronous | FileOptions.SequentialScan);
- await using (fileStream.ConfigureAwait(false))
- {
- using var reader = new StreamReader(fileStream);
- var count = 0;
+ using var reader = new StreamReader(fileStream);
+ var count = 0;
- while (!reader.EndOfStream)
+ while (!reader.EndOfStream)
+ {
+ var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
+ if (line is null)
{
- var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
- if (line is null)
- {
- // Nothing currently in buffer.
- break;
- }
+ // Nothing currently in buffer.
+ break;
+ }
- if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ count++;
+ if (count >= segmentCount)
{
- count++;
- if (count >= segmentCount)
- {
- logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
- return;
- }
+ logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
+ return;
}
}
}
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- catch (IOException)
- {
- // May get an error if the file is locked
}
- await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
- }
-
- /// <summary>
- /// Gets the #EXT-X-MAP string.
- /// </summary>
- /// <param name="outputPath">The output path of the file.</param>
- /// <param name="state">The <see cref="StreamState"/>.</param>
- /// <param name="isOsDepends">Get a normal string or depends on OS.</param>
- /// <returns>The string text of #EXT-X-MAP.</returns>
- public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
- {
- var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
- var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
- var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
- var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
-
- // on Linux/Unix
- // #EXT-X-MAP:URI="prefix-1.mp4"
- var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension;
- if (!isOsDepends)
+ catch (IOException)
{
- return fmp4InitFileName;
+ // May get an error if the file is locked
}
- if (OperatingSystem.IsWindows())
- {
- // on Windows
- // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
- fmp4InitFileName = outputPrefix + "-1" + outputExtension;
- }
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ /// <summary>
+ /// Gets the #EXT-X-MAP string.
+ /// </summary>
+ /// <param name="outputPath">The output path of the file.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <param name="isOsDepends">Get a normal string or depends on OS.</param>
+ /// <returns>The string text of #EXT-X-MAP.</returns>
+ public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
+ {
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+
+ // on Linux/Unix
+ // #EXT-X-MAP:URI="prefix-1.mp4"
+ var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension;
+ if (!isOsDepends)
+ {
return fmp4InitFileName;
}
- /// <summary>
- /// Gets the hls playlist text.
- /// </summary>
- /// <param name="path">The path to the playlist file.</param>
- /// <param name="state">The <see cref="StreamState"/>.</param>
- /// <returns>The playlist text as a string.</returns>
- public static string GetLivePlaylistText(string path, StreamState state)
+ if (OperatingSystem.IsWindows())
{
- var text = File.ReadAllText(path);
+ // on Windows
+ // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
+ fmp4InitFileName = outputPrefix + "-1" + outputExtension;
+ }
- var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
- if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- var fmp4InitFileName = GetFmp4InitFileName(path, state, true);
- var baseUrlParam = string.Format(
- CultureInfo.InvariantCulture,
- "hls/{0}/",
- Path.GetFileNameWithoutExtension(path));
- var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false);
+ return fmp4InitFileName;
+ }
- // Replace fMP4 init file URI.
- text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
- }
+ /// <summary>
+ /// Gets the hls playlist text.
+ /// </summary>
+ /// <param name="path">The path to the playlist file.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The playlist text as a string.</returns>
+ public static string GetLivePlaylistText(string path, StreamState state)
+ {
+ var text = File.ReadAllText(path);
+
+ var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+ if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var fmp4InitFileName = GetFmp4InitFileName(path, state, true);
+ var baseUrlParam = string.Format(
+ CultureInfo.InvariantCulture,
+ "hls/{0}/",
+ Path.GetFileNameWithoutExtension(path));
+ var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false);
- return text;
+ // Replace fMP4 init file URI.
+ text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
}
+
+ return text;
}
}
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index e0245fe4d..5910d8073 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -25,476 +25,475 @@ using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Media info helper.
+/// </summary>
+public class MediaInfoHelper
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ILogger<MediaInfoHelper> _logger;
+ private readonly INetworkManager _networkManager;
+ private readonly IDeviceManager _deviceManager;
+
/// <summary>
- /// Media info helper.
+ /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
/// </summary>
- public class MediaInfoHelper
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ public MediaInfoHelper(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IMediaSourceManager mediaSourceManager,
+ IMediaEncoder mediaEncoder,
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<MediaInfoHelper> logger,
+ INetworkManager networkManager,
+ IDeviceManager deviceManager)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly ILogger<MediaInfoHelper> _logger;
- private readonly INetworkManager _networkManager;
- private readonly IDeviceManager _deviceManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- public MediaInfoHelper(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IMediaSourceManager mediaSourceManager,
- IMediaEncoder mediaEncoder,
- IServerConfigurationManager serverConfigurationManager,
- ILogger<MediaInfoHelper> logger,
- INetworkManager networkManager,
- IDeviceManager deviceManager)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
- _mediaEncoder = mediaEncoder;
- _serverConfigurationManager = serverConfigurationManager;
- _logger = logger;
- _networkManager = networkManager;
- _deviceManager = deviceManager;
- }
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _mediaSourceManager = mediaSourceManager;
+ _mediaEncoder = mediaEncoder;
+ _serverConfigurationManager = serverConfigurationManager;
+ _logger = logger;
+ _networkManager = networkManager;
+ _deviceManager = deviceManager;
+ }
- /// <summary>
- /// Get playback info.
- /// </summary>
- /// <param name="id">Item id.</param>
- /// <param name="userId">User Id.</param>
- /// <param name="mediaSourceId">Media source id.</param>
- /// <param name="liveStreamId">Live stream id.</param>
- /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
- public async Task<PlaybackInfoResponse> GetPlaybackInfo(
- Guid id,
- Guid? userId,
- string? mediaSourceId = null,
- string? liveStreamId = null)
+ /// <summary>
+ /// Get playback info.
+ /// </summary>
+ /// <param name="id">Item id.</param>
+ /// <param name="userId">User Id.</param>
+ /// <param name="mediaSourceId">Media source id.</param>
+ /// <param name="liveStreamId">Live stream id.</param>
+ /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
+ public async Task<PlaybackInfoResponse> GetPlaybackInfo(
+ Guid id,
+ Guid? userId,
+ string? mediaSourceId = null,
+ string? liveStreamId = null)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(id);
+ var result = new PlaybackInfoResponse();
+
+ MediaSourceInfo[] mediaSources;
+ if (string.IsNullOrWhiteSpace(liveStreamId))
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = _libraryManager.GetItemById(id);
- var result = new PlaybackInfoResponse();
-
- MediaSourceInfo[] mediaSources;
- if (string.IsNullOrWhiteSpace(liveStreamId))
- {
- // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
- var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
-
- if (string.IsNullOrWhiteSpace(mediaSourceId))
- {
- mediaSources = mediaSourcesList.ToArray();
- }
- else
- {
- mediaSources = mediaSourcesList
- .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
- .ToArray();
- }
- }
- else
- {
- var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+ // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
+ var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
- mediaSources = new[] { mediaSource };
- }
-
- if (mediaSources.Length == 0)
+ if (string.IsNullOrWhiteSpace(mediaSourceId))
{
- result.MediaSources = Array.Empty<MediaSourceInfo>();
-
- result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
+ mediaSources = mediaSourcesList.ToArray();
}
else
{
- // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
- // Should we move this directly into MediaSourceManager?
- var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
- if (mediaSourcesClone is not null)
- {
- result.MediaSources = mediaSourcesClone;
- }
-
- result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ mediaSources = mediaSourcesList
+ .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
}
+ }
+ else
+ {
+ var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
- return result;
+ mediaSources = new[] { mediaSource };
}
- /// <summary>
- /// SetDeviceSpecificData.
- /// </summary>
- /// <param name="item">Item to set data for.</param>
- /// <param name="mediaSource">Media source info.</param>
- /// <param name="profile">Device profile.</param>
- /// <param name="claimsPrincipal">Current claims principal.</param>
- /// <param name="maxBitrate">Max bitrate.</param>
- /// <param name="startTimeTicks">Start time ticks.</param>
- /// <param name="mediaSourceId">Media source id.</param>
- /// <param name="audioStreamIndex">Audio stream index.</param>
- /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
- /// <param name="maxAudioChannels">Max audio channels.</param>
- /// <param name="playSessionId">Play session id.</param>
- /// <param name="userId">User id.</param>
- /// <param name="enableDirectPlay">Enable direct play.</param>
- /// <param name="enableDirectStream">Enable direct stream.</param>
- /// <param name="enableTranscoding">Enable transcoding.</param>
- /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
- /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
- /// <param name="ipAddress">Requesting IP address.</param>
- public void SetDeviceSpecificData(
- BaseItem item,
- MediaSourceInfo mediaSource,
- DeviceProfile profile,
- ClaimsPrincipal claimsPrincipal,
- int? maxBitrate,
- long startTimeTicks,
- string mediaSourceId,
- int? audioStreamIndex,
- int? subtitleStreamIndex,
- int? maxAudioChannels,
- string playSessionId,
- Guid userId,
- bool enableDirectPlay,
- bool enableDirectStream,
- bool enableTranscoding,
- bool allowVideoStreamCopy,
- bool allowAudioStreamCopy,
- IPAddress ipAddress)
+ if (mediaSources.Length == 0)
{
- var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
+ result.MediaSources = Array.Empty<MediaSourceInfo>();
- var options = new MediaOptions
- {
- MediaSources = new[] { mediaSource },
- Context = EncodingContext.Streaming,
- DeviceId = claimsPrincipal.GetDeviceId(),
- ItemId = item.Id,
- Profile = profile,
- MaxAudioChannels = maxAudioChannels,
- AllowAudioStreamCopy = allowAudioStreamCopy,
- AllowVideoStreamCopy = allowVideoStreamCopy
- };
-
- if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+ result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
+ }
+ else
+ {
+ // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
+ // Should we move this directly into MediaSourceManager?
+ var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
+ if (mediaSourcesClone is not null)
{
- options.MediaSourceId = mediaSourceId;
- options.AudioStreamIndex = audioStreamIndex;
- options.SubtitleStreamIndex = subtitleStreamIndex;
+ result.MediaSources = mediaSourcesClone;
}
- var user = _userManager.GetUserById(userId);
+ result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ }
- if (!enableDirectPlay)
- {
- mediaSource.SupportsDirectPlay = false;
- }
+ return result;
+ }
- if (!enableDirectStream || !allowVideoStreamCopy)
- {
- mediaSource.SupportsDirectStream = false;
- }
+ /// <summary>
+ /// SetDeviceSpecificData.
+ /// </summary>
+ /// <param name="item">Item to set data for.</param>
+ /// <param name="mediaSource">Media source info.</param>
+ /// <param name="profile">Device profile.</param>
+ /// <param name="claimsPrincipal">Current claims principal.</param>
+ /// <param name="maxBitrate">Max bitrate.</param>
+ /// <param name="startTimeTicks">Start time ticks.</param>
+ /// <param name="mediaSourceId">Media source id.</param>
+ /// <param name="audioStreamIndex">Audio stream index.</param>
+ /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
+ /// <param name="maxAudioChannels">Max audio channels.</param>
+ /// <param name="playSessionId">Play session id.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="enableDirectPlay">Enable direct play.</param>
+ /// <param name="enableDirectStream">Enable direct stream.</param>
+ /// <param name="enableTranscoding">Enable transcoding.</param>
+ /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
+ /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
+ /// <param name="ipAddress">Requesting IP address.</param>
+ public void SetDeviceSpecificData(
+ BaseItem item,
+ MediaSourceInfo mediaSource,
+ DeviceProfile profile,
+ ClaimsPrincipal claimsPrincipal,
+ int? maxBitrate,
+ long startTimeTicks,
+ string mediaSourceId,
+ int? audioStreamIndex,
+ int? subtitleStreamIndex,
+ int? maxAudioChannels,
+ string playSessionId,
+ Guid userId,
+ bool enableDirectPlay,
+ bool enableDirectStream,
+ bool enableTranscoding,
+ bool allowVideoStreamCopy,
+ bool allowAudioStreamCopy,
+ IPAddress ipAddress)
+ {
+ var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
- if (!enableTranscoding)
- {
- mediaSource.SupportsTranscoding = false;
- }
+ var options = new MediaOptions
+ {
+ MediaSources = new[] { mediaSource },
+ Context = EncodingContext.Streaming,
+ DeviceId = claimsPrincipal.GetDeviceId(),
+ ItemId = item.Id,
+ Profile = profile,
+ MaxAudioChannels = maxAudioChannels,
+ AllowAudioStreamCopy = allowAudioStreamCopy,
+ AllowVideoStreamCopy = allowVideoStreamCopy
+ };
+
+ if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ options.MediaSourceId = mediaSourceId;
+ options.AudioStreamIndex = audioStreamIndex;
+ options.SubtitleStreamIndex = subtitleStreamIndex;
+ }
- if (item is Audio)
- {
- _logger.LogInformation(
- "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
- user.Username,
- user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
- }
- else
- {
- _logger.LogInformation(
- "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
- user.Username,
- user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
- user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
- user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
- }
+ var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
+ if (!enableDirectPlay)
+ {
+ mediaSource.SupportsDirectPlay = false;
+ }
- if (!options.ForceDirectStream)
- {
- // direct-stream http streaming is currently broken
- options.EnableDirectStream = false;
- }
+ if (!enableDirectStream || !allowVideoStreamCopy)
+ {
+ mediaSource.SupportsDirectStream = false;
+ }
- // Beginning of Playback Determination
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.GetOptimalAudioStream(options)
- : streamBuilder.GetOptimalVideoStream(options);
+ if (!enableTranscoding)
+ {
+ mediaSource.SupportsTranscoding = false;
+ }
- if (streamInfo is not null)
- {
- streamInfo.PlaySessionId = playSessionId;
- streamInfo.StartPositionTicks = startTimeTicks;
+ if (item is Audio)
+ {
+ _logger.LogInformation(
+ "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
+ user.Username,
+ user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+ }
+ else
+ {
+ _logger.LogInformation(
+ "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
+ user.Username,
+ user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+ user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+ user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+ }
- mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
+ options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
- // Players do not handle this being set according to PlayMethod
- mediaSource.SupportsDirectStream =
- options.EnableDirectStream
- ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
- : streamInfo.PlayMethod == PlayMethod.DirectPlay;
+ if (!options.ForceDirectStream)
+ {
+ // direct-stream http streaming is currently broken
+ options.EnableDirectStream = false;
+ }
- mediaSource.SupportsTranscoding =
- streamInfo.PlayMethod == PlayMethod.DirectStream
- || mediaSource.TranscodingContainer is not null
- || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
+ // Beginning of Playback Determination
+ var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ ? streamBuilder.GetOptimalAudioStream(options)
+ : streamBuilder.GetOptimalVideoStream(options);
- if (item is Audio)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
- {
- mediaSource.SupportsTranscoding = false;
- }
- }
- else if (item is Video)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
- {
- mediaSource.SupportsTranscoding = false;
- }
- }
+ if (streamInfo is not null)
+ {
+ streamInfo.PlaySessionId = playSessionId;
+ streamInfo.StartPositionTicks = startTimeTicks;
- if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
- {
- mediaSource.SupportsDirectPlay = false;
- mediaSource.SupportsDirectStream = false;
+ mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
- }
- else
+ // Players do not handle this being set according to PlayMethod
+ mediaSource.SupportsDirectStream =
+ options.EnableDirectStream
+ ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
+ : streamInfo.PlayMethod == PlayMethod.DirectPlay;
+
+ mediaSource.SupportsTranscoding =
+ streamInfo.PlayMethod == PlayMethod.DirectStream
+ || mediaSource.TranscodingContainer is not null
+ || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
+
+ if (item is Audio)
+ {
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
- if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
- {
- streamInfo.PlayMethod = PlayMethod.Transcode;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
-
- if (!allowVideoStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- }
-
- if (!allowAudioStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- }
- }
+ mediaSource.SupportsTranscoding = false;
}
-
- // Do this after the above so that StartPositionTicks is set
- // The token must not be null
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
- mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
-
- foreach (var attachment in mediaSource.MediaAttachments)
+ else if (item is Video)
{
- attachment.DeliveryUrl = string.Format(
- CultureInfo.InvariantCulture,
- "/Videos/{0}/{1}/Attachments/{2}",
- item.Id,
- mediaSource.Id,
- attachment.Index);
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+ {
+ mediaSource.SupportsTranscoding = false;
+ }
}
- }
- /// <summary>
- /// Sort media source.
- /// </summary>
- /// <param name="result">Playback info response.</param>
- /// <param name="maxBitrate">Max bitrate.</param>
- public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
- {
- var originalList = result.MediaSources.ToList();
+ if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+ {
+ mediaSource.SupportsDirectPlay = false;
+ mediaSource.SupportsDirectStream = false;
- result.MediaSources = result.MediaSources.OrderBy(i =>
+ mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
+ mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+ mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+ mediaSource.TranscodingContainer = streamInfo.Container;
+ mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+ }
+ else
+ {
+ if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
{
- // Nothing beats direct playing a file
- if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
- {
- return 0;
- }
+ streamInfo.PlayMethod = PlayMethod.Transcode;
+ mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
- return 1;
- })
- .ThenBy(i =>
- {
- // Let's assume direct streaming a file is just as desirable as direct playing a remote url
- if (i.SupportsDirectPlay || i.SupportsDirectStream)
+ if (!allowVideoStreamCopy)
{
- return 0;
+ mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
}
- return 1;
- })
- .ThenBy(i =>
- {
- return i.Protocol switch
+ if (!allowAudioStreamCopy)
{
- MediaProtocol.File => 0,
- _ => 1,
- };
- })
- .ThenBy(i =>
- {
- if (maxBitrate.HasValue && i.Bitrate.HasValue)
- {
- return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
+ mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
+ }
+ }
- return 1;
- })
- .ThenBy(originalList.IndexOf)
- .ToArray();
+ // Do this after the above so that StartPositionTicks is set
+ // The token must not be null
+ SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
- /// <summary>
- /// Open media source.
- /// </summary>
- /// <param name="httpContext">Http Context.</param>
- /// <param name="request">Live stream request.</param>
- /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
- public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
+ foreach (var attachment in mediaSource.MediaAttachments)
{
- var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
+ attachment.DeliveryUrl = string.Format(
+ CultureInfo.InvariantCulture,
+ "/Videos/{0}/{1}/Attachments/{2}",
+ item.Id,
+ mediaSource.Id,
+ attachment.Index);
+ }
+ }
- var profile = request.DeviceProfile;
- if (profile is null)
+ /// <summary>
+ /// Sort media source.
+ /// </summary>
+ /// <param name="result">Playback info response.</param>
+ /// <param name="maxBitrate">Max bitrate.</param>
+ public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
+ {
+ var originalList = result.MediaSources.ToList();
+
+ result.MediaSources = result.MediaSources.OrderBy(i =>
{
- var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
- if (clientCapabilities is not null)
+ // Nothing beats direct playing a file
+ if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
{
- profile = clientCapabilities.DeviceProfile;
+ return 0;
}
- }
- if (profile is not null)
+ return 1;
+ })
+ .ThenBy(i =>
{
- var item = _libraryManager.GetItemById(request.ItemId);
-
- SetDeviceSpecificData(
- item,
- result.MediaSource,
- profile,
- httpContext.User,
- request.MaxStreamingBitrate,
- request.StartTimeTicks ?? 0,
- result.MediaSource.Id,
- request.AudioStreamIndex,
- request.SubtitleStreamIndex,
- request.MaxAudioChannels,
- request.PlaySessionId,
- request.UserId,
- request.EnableDirectPlay,
- request.EnableDirectStream,
- true,
- true,
- true,
- httpContext.GetNormalizedRemoteIp());
- }
- else
+ // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+ if (i.SupportsDirectPlay || i.SupportsDirectStream)
+ {
+ return 0;
+ }
+
+ return 1;
+ })
+ .ThenBy(i =>
+ {
+ return i.Protocol switch
+ {
+ MediaProtocol.File => 0,
+ _ => 1,
+ };
+ })
+ .ThenBy(i =>
{
- if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+ if (maxBitrate.HasValue && i.Bitrate.HasValue)
{
- result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+ return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
}
- }
- // here was a check if (result.MediaSource is not null) but Rider said it will never be null
- NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
+ return 1;
+ })
+ .ThenBy(originalList.IndexOf)
+ .ToArray();
+ }
- return result;
- }
+ /// <summary>
+ /// Open media source.
+ /// </summary>
+ /// <param name="httpContext">Http Context.</param>
+ /// <param name="request">Live stream request.</param>
+ /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
+ public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
+ {
+ var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
- /// <summary>
- /// Normalize media source container.
- /// </summary>
- /// <param name="mediaSource">Media source.</param>
- /// <param name="profile">Device profile.</param>
- /// <param name="type">Dlna profile type.</param>
- public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
+ var profile = request.DeviceProfile;
+ if (profile is null)
{
- mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
+ var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
+ if (clientCapabilities is not null)
+ {
+ profile = clientCapabilities.DeviceProfile;
+ }
}
- private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
+ if (profile is not null)
+ {
+ var item = _libraryManager.GetItemById(request.ItemId);
+
+ SetDeviceSpecificData(
+ item,
+ result.MediaSource,
+ profile,
+ httpContext.User,
+ request.MaxStreamingBitrate,
+ request.StartTimeTicks ?? 0,
+ result.MediaSource.Id,
+ request.AudioStreamIndex,
+ request.SubtitleStreamIndex,
+ request.MaxAudioChannels,
+ request.PlaySessionId,
+ request.UserId,
+ request.EnableDirectPlay,
+ request.EnableDirectStream,
+ true,
+ true,
+ true,
+ httpContext.GetNormalizedRemoteIp());
+ }
+ else
{
- var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
- mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+ if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+ {
+ result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+ }
+ }
+
+ // here was a check if (result.MediaSource is not null) but Rider said it will never be null
+ NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
- mediaSource.TranscodeReasons = info.TranscodeReasons;
+ return result;
+ }
- foreach (var profile in profiles)
+ /// <summary>
+ /// Normalize media source container.
+ /// </summary>
+ /// <param name="mediaSource">Media source.</param>
+ /// <param name="profile">Device profile.</param>
+ /// <param name="type">Dlna profile type.</param>
+ public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
+ {
+ mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
+ }
+
+ private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
+ {
+ var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
+ mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+
+ mediaSource.TranscodeReasons = info.TranscodeReasons;
+
+ foreach (var profile in profiles)
+ {
+ foreach (var stream in mediaSource.MediaStreams)
{
- foreach (var stream in mediaSource.MediaStreams)
+ if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
{
- if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
- {
- stream.DeliveryMethod = profile.DeliveryMethod;
+ stream.DeliveryMethod = profile.DeliveryMethod;
- if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
- {
- stream.DeliveryUrl = profile.Url.TrimStart('-');
- stream.IsExternalUrl = profile.IsExternalUrl;
- }
+ if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
+ {
+ stream.DeliveryUrl = profile.Url.TrimStart('-');
+ stream.IsExternalUrl = profile.IsExternalUrl;
}
}
}
}
+ }
+
+ private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
+ {
+ var maxBitrate = clientMaxBitrate;
+ var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
- private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
+ if (remoteClientMaxBitrate <= 0)
{
- var maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
+ remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
+ }
- if (remoteClientMaxBitrate <= 0)
- {
- remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
- }
+ if (remoteClientMaxBitrate > 0)
+ {
+ var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
- if (remoteClientMaxBitrate > 0)
+ _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
+ if (!isInLocalNetwork)
{
- var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
-
- _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
- if (!isInLocalNetwork)
- {
- maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
- }
+ maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
}
-
- return maxBitrate;
}
+
+ return maxBitrate;
}
}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index dfeeea2b0..d7b1c9f8b 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -6,178 +6,177 @@ using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
using MediaBrowser.Model.IO;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// A progressive file stream for transferring transcoded files as they are written to.
+/// </summary>
+public class ProgressiveFileStream : Stream
{
+ private readonly Stream _stream;
+ private readonly TranscodingJobDto? _job;
+ private readonly TranscodingJobHelper? _transcodingJobHelper;
+ private readonly int _timeoutMs;
+ private bool _disposed;
+
/// <summary>
- /// A progressive file stream for transferring transcoded files as they are written to.
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
/// </summary>
- public class ProgressiveFileStream : Stream
+ /// <param name="filePath">The path to the transcoded file.</param>
+ /// <param name="job">The transcoding job information.</param>
+ /// <param name="transcodingJobHelper">The transcoding job helper.</param>
+ /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
+ public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000)
{
- private readonly Stream _stream;
- private readonly TranscodingJobDto? _job;
- private readonly TranscodingJobHelper? _transcodingJobHelper;
- private readonly int _timeoutMs;
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
- /// </summary>
- /// <param name="filePath">The path to the transcoded file.</param>
- /// <param name="job">The transcoding job information.</param>
- /// <param name="transcodingJobHelper">The transcoding job helper.</param>
- /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
- public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000)
- {
- _job = job;
- _transcodingJobHelper = transcodingJobHelper;
- _timeoutMs = timeoutMs;
+ _job = job;
+ _transcodingJobHelper = transcodingJobHelper;
+ _timeoutMs = timeoutMs;
- _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
- }
+ _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
- /// </summary>
- /// <param name="stream">The stream to progressively copy.</param>
- /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
- public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
- {
- _job = null;
- _transcodingJobHelper = null;
- _timeoutMs = timeoutMs;
- _stream = stream;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+ /// </summary>
+ /// <param name="stream">The stream to progressively copy.</param>
+ /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
+ public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
+ {
+ _job = null;
+ _transcodingJobHelper = null;
+ _timeoutMs = timeoutMs;
+ _stream = stream;
+ }
- /// <inheritdoc />
- public override bool CanRead => _stream.CanRead;
+ /// <inheritdoc />
+ public override bool CanRead => _stream.CanRead;
- /// <inheritdoc />
- public override bool CanSeek => false;
+ /// <inheritdoc />
+ public override bool CanSeek => false;
- /// <inheritdoc />
- public override bool CanWrite => false;
+ /// <inheritdoc />
+ public override bool CanWrite => false;
- /// <inheritdoc />
- public override long Length => throw new NotSupportedException();
+ /// <inheritdoc />
+ public override long Length => throw new NotSupportedException();
- /// <inheritdoc />
- public override long Position
- {
- get => throw new NotSupportedException();
- set => throw new NotSupportedException();
- }
+ /// <inheritdoc />
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
- /// <inheritdoc />
- public override void Flush()
- {
- // Not supported
- }
+ /// <inheritdoc />
+ public override void Flush()
+ {
+ // Not supported
+ }
- /// <inheritdoc />
- public override int Read(byte[] buffer, int offset, int count)
- => Read(buffer.AsSpan(offset, count));
+ /// <inheritdoc />
+ public override int Read(byte[] buffer, int offset, int count)
+ => Read(buffer.AsSpan(offset, count));
- /// <inheritdoc />
- public override int Read(Span<byte> buffer)
- {
- int totalBytesRead = 0;
- var stopwatch = Stopwatch.StartNew();
+ /// <inheritdoc />
+ public override int Read(Span<byte> buffer)
+ {
+ int totalBytesRead = 0;
+ var stopwatch = Stopwatch.StartNew();
- while (true)
+ while (true)
+ {
+ totalBytesRead += _stream.Read(buffer);
+ if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
{
- totalBytesRead += _stream.Read(buffer);
- if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
- {
- break;
- }
-
- Thread.Sleep(50);
+ break;
}
- UpdateBytesWritten(totalBytesRead);
-
- return totalBytesRead;
+ Thread.Sleep(50);
}
- /// <inheritdoc />
- public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
+ UpdateBytesWritten(totalBytesRead);
- /// <inheritdoc />
- public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
- {
- int totalBytesRead = 0;
- var stopwatch = Stopwatch.StartNew();
+ return totalBytesRead;
+ }
- while (true)
- {
- totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
- {
- break;
- }
+ /// <inheritdoc />
+ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
- await Task.Delay(50, cancellationToken).ConfigureAwait(false);
- }
+ /// <inheritdoc />
+ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ int totalBytesRead = 0;
+ var stopwatch = Stopwatch.StartNew();
- UpdateBytesWritten(totalBytesRead);
+ while (true)
+ {
+ totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+ if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
+ {
+ break;
+ }
- return totalBytesRead;
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
- /// <inheritdoc />
- public override long Seek(long offset, SeekOrigin origin)
- => throw new NotSupportedException();
+ UpdateBytesWritten(totalBytesRead);
+
+ return totalBytesRead;
+ }
+
+ /// <inheritdoc />
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException();
- /// <inheritdoc />
- public override void SetLength(long value)
- => throw new NotSupportedException();
+ /// <inheritdoc />
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
- /// <inheritdoc />
- public override void Write(byte[] buffer, int offset, int count)
- => throw new NotSupportedException();
+ /// <inheritdoc />
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
- /// <inheritdoc />
- protected override void Dispose(bool disposing)
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
{
- if (_disposed)
- {
- return;
- }
+ return;
+ }
- try
+ try
+ {
+ if (disposing)
{
- if (disposing)
- {
- _stream.Dispose();
+ _stream.Dispose();
- if (_job is not null)
- {
- _transcodingJobHelper?.OnTranscodeEndRequest(_job);
- }
+ if (_job is not null)
+ {
+ _transcodingJobHelper?.OnTranscodeEndRequest(_job);
}
}
- finally
- {
- _disposed = true;
- base.Dispose(disposing);
- }
}
-
- private void UpdateBytesWritten(int totalBytesRead)
+ finally
{
- if (_job is not null)
- {
- _job.BytesDownloaded += totalBytesRead;
- }
+ _disposed = true;
+ base.Dispose(disposing);
}
+ }
- private bool StopReading(int bytesRead, long elapsed)
+ private void UpdateBytesWritten(int totalBytesRead)
+ {
+ if (_job is not null)
{
- // It should stop reading when anything has been successfully read or if the job has exited
- // If the job is null, however, it's a live stream and will require user action to close,
- // but don't keep it open indefinitely if it isn't reading anything
- return bytesRead > 0 || (_job?.HasExited ?? elapsed >= _timeoutMs);
+ _job.BytesDownloaded += totalBytesRead;
}
}
+
+ private bool StopReading(int bytesRead, long elapsed)
+ {
+ // It should stop reading when anything has been successfully read or if the job has exited
+ // If the job is null, however, it's a live stream and will require user action to close,
+ // but don't keep it open indefinitely if it isn't reading anything
+ return bytesRead > 0 || (_job?.HasExited ?? elapsed >= _timeoutMs);
+ }
}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 035d84513..57098edba 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -11,138 +11,169 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Request Extensions.
+/// </summary>
+public static class RequestHelpers
{
/// <summary>
- /// Request Extensions.
+ /// Get Order By.
/// </summary>
- public static class RequestHelpers
+ /// <param name="sortBy">Sort By. Comma delimited string.</param>
+ /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
+ /// <returns>Order By.</returns>
+ public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
{
- /// <summary>
- /// Get Order By.
- /// </summary>
- /// <param name="sortBy">Sort By. Comma delimited string.</param>
- /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
- /// <returns>Order By.</returns>
- public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
+ if (sortBy.Count == 0)
{
- if (sortBy.Count == 0)
- {
- return Array.Empty<(string, SortOrder)>();
- }
+ return Array.Empty<(string, SortOrder)>();
+ }
- var result = new (string, SortOrder)[sortBy.Count];
- var i = 0;
- // Add elements which have a SortOrder specified
- for (; i < requestedSortOrder.Count; i++)
- {
- result[i] = (sortBy[i], requestedSortOrder[i]);
- }
+ var result = new (string, SortOrder)[sortBy.Count];
+ var i = 0;
+ // Add elements which have a SortOrder specified
+ for (; i < requestedSortOrder.Count; i++)
+ {
+ result[i] = (sortBy[i], requestedSortOrder[i]);
+ }
- // Add remaining elements with the first specified SortOrder
- // or the default one if no SortOrders are specified
- var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
- for (; i < sortBy.Count; i++)
- {
- result[i] = (sortBy[i], order);
- }
+ // Add remaining elements with the first specified SortOrder
+ // or the default one if no SortOrders are specified
+ var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
+ for (; i < sortBy.Count; i++)
+ {
+ result[i] = (sortBy[i], order);
+ }
- return result;
+ return result;
+ }
+
+ /// <summary>
+ /// Checks if the user can access a user.
+ /// </summary>
+ /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>A <see cref="bool"/> whether the user can access the user.</returns>
+ internal static Guid GetUserId(ClaimsPrincipal claimsPrincipal, Guid? userId)
+ {
+ var authenticatedUserId = claimsPrincipal.GetUserId();
+
+ // UserId not provided, fall back to authenticated user id.
+ if (userId is null || userId.Value.Equals(default))
+ {
+ return authenticatedUserId;
}
- /// <summary>
- /// Checks if the user can update an entry.
- /// </summary>
- /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
- /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
- internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences)
+ // User must be administrator to access another user.
+ var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
+ if (!userId.Value.Equals(authenticatedUserId) && !isAdministrator)
{
- var authenticatedUserId = claimsPrincipal.GetUserId();
- var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
+ throw new SecurityException("Forbidden");
+ }
- // If they're going to update the record of another user, they must be an administrator
- if (!userId.Equals(authenticatedUserId) && !isAdministrator)
- {
- return false;
- }
+ return userId.Value;
+ }
- // TODO the EnableUserPreferenceAccess policy does not seem to be used elsewhere
- if (!restrictUserPreferences || isAdministrator)
- {
- return true;
- }
+ /// <summary>
+ /// Checks if the user can update an entry.
+ /// </summary>
+ /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
+ /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
+ internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences)
+ {
+ var authenticatedUserId = claimsPrincipal.GetUserId();
+ var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
- var user = userManager.GetUserById(userId);
- return user.EnableUserPreferenceAccess;
+ // If they're going to update the record of another user, they must be an administrator
+ if (!userId.Equals(authenticatedUserId) && !isAdministrator)
+ {
+ return false;
}
- internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ // TODO the EnableUserPreferenceAccess policy does not seem to be used elsewhere
+ if (!restrictUserPreferences || isAdministrator)
{
- var userId = httpContext.User.GetUserId();
- var user = userManager.GetUserById(userId);
- var session = await sessionManager.LogSessionActivity(
- httpContext.User.GetClient(),
- httpContext.User.GetVersion(),
- httpContext.User.GetDeviceId(),
- httpContext.User.GetDevice(),
- httpContext.GetNormalizedRemoteIp().ToString(),
- user).ConfigureAwait(false);
-
- if (session is null)
- {
- throw new ArgumentException("Session not found.");
- }
-
- return session;
+ return true;
}
- internal static async Task<string> GetSessionId(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ var user = userManager.GetUserById(userId);
+ if (user is null)
{
- var session = await GetSession(sessionManager, userManager, httpContext).ConfigureAwait(false);
+ throw new ResourceNotFoundException();
+ }
+
+ return user.EnableUserPreferenceAccess;
+ }
- return session.Id;
+ internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ {
+ var userId = httpContext.User.GetUserId();
+ var user = userManager.GetUserById(userId);
+ var session = await sessionManager.LogSessionActivity(
+ httpContext.User.GetClient(),
+ httpContext.User.GetVersion(),
+ httpContext.User.GetDeviceId(),
+ httpContext.User.GetDevice(),
+ httpContext.GetNormalizedRemoteIp().ToString(),
+ user).ConfigureAwait(false);
+
+ if (session is null)
+ {
+ throw new ResourceNotFoundException("Session not found.");
}
- internal static QueryResult<BaseItemDto> CreateQueryResult(
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result,
- DtoOptions dtoOptions,
- IDtoService dtoService,
- bool includeItemTypes,
- User? user)
+ return session;
+ }
+
+ internal static async Task<string> GetSessionId(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ {
+ var session = await GetSession(sessionManager, userManager, httpContext).ConfigureAwait(false);
+
+ return session.Id;
+ }
+
+ internal static QueryResult<BaseItemDto> CreateQueryResult(
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result,
+ DtoOptions dtoOptions,
+ IDtoService dtoService,
+ bool includeItemTypes,
+ User? user)
+ {
+ var dtos = result.Items.Select(i =>
{
- var dtos = result.Items.Select(i =>
+ var (baseItem, counts) = i;
+ var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (includeItemTypes)
{
- var (baseItem, counts) = i;
- var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (includeItemTypes)
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>(
- result.StartIndex,
- result.TotalRecordCount,
- dtos.ToArray());
- }
+ dto.ChildCount = counts.ItemCount;
+ dto.ProgramCount = counts.ProgramCount;
+ dto.SeriesCount = counts.SeriesCount;
+ dto.EpisodeCount = counts.EpisodeCount;
+ dto.MovieCount = counts.MovieCount;
+ dto.TrailerCount = counts.TrailerCount;
+ dto.AlbumCount = counts.AlbumCount;
+ dto.SongCount = counts.SongCount;
+ dto.ArtistCount = counts.ArtistCount;
+ }
+
+ return dto;
+ });
+
+ return new QueryResult<BaseItemDto>(
+ result.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index d4fc9c020..9b5a14c4d 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -22,761 +22,760 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// The streaming helpers.
+/// </summary>
+public static class StreamingHelpers
{
/// <summary>
- /// The streaming helpers.
+ /// Gets the current streaming state.
/// </summary>
- public static class StreamingHelpers
+ /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
+ /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> 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="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+ /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
+ public static async Task<StreamState> GetStreamingState(
+ StreamingRequestDto streamingRequest,
+ HttpContext httpContext,
+ IMediaSourceManager mediaSourceManager,
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ EncodingHelper encodingHelper,
+ IDlnaManager dlnaManager,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ TranscodingJobType transcodingJobType,
+ CancellationToken cancellationToken)
{
- /// <summary>
- /// Gets the current streaming state.
- /// </summary>
- /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
- /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> 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="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>
- /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
- /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
- /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
- public static async Task<StreamState> GetStreamingState(
- StreamingRequestDto streamingRequest,
- HttpContext httpContext,
- IMediaSourceManager mediaSourceManager,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- EncodingHelper encodingHelper,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- TranscodingJobType transcodingJobType,
- CancellationToken cancellationToken)
- {
- var httpRequest = httpContext.Request;
- // Parse the DLNA time seek header
- if (!streamingRequest.StartTimeTicks.HasValue)
- {
- var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
+ var httpRequest = httpContext.Request;
+ // Parse the DLNA time seek header
+ if (!streamingRequest.StartTimeTicks.HasValue)
+ {
+ var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
- streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
- }
+ streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
+ }
- if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
- {
- ParseParams(streamingRequest);
- }
+ if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
+ {
+ ParseParams(streamingRequest);
+ }
- streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
- if (httpRequest.Path.Value is null)
- {
- throw new ResourceNotFoundException(nameof(httpRequest.Path));
- }
+ streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
+ if (httpRequest.Path.Value is null)
+ {
+ throw new ResourceNotFoundException(nameof(httpRequest.Path));
+ }
- var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
+ var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
- if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
- {
- streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
- }
+ if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
+ {
+ streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
+ }
- var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
- streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||
- string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
+ var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
+ streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||
+ string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
- var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
- {
- Request = streamingRequest,
- RequestedUrl = url,
- UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
- EnableDlnaHeaders = enableDlnaHeaders
- };
-
- var userId = httpContext.User.GetUserId();
- if (!userId.Equals(default))
- {
- state.User = userManager.GetUserById(userId);
- }
+ var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
+ {
+ Request = streamingRequest,
+ RequestedUrl = url,
+ UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
+ EnableDlnaHeaders = enableDlnaHeaders
+ };
+
+ var userId = httpContext.User.GetUserId();
+ if (!userId.Equals(default))
+ {
+ state.User = userManager.GetUserById(userId);
+ }
- if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
- {
- state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
- }
+ if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
+ {
+ state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+ }
- if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
- {
- state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)
- ?? state.SupportedAudioCodecs.FirstOrDefault();
- }
+ if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
+ {
+ state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)
+ ?? state.SupportedAudioCodecs.FirstOrDefault();
+ }
- if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
- {
- state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec)
- ?? state.SupportedSubtitleCodecs.FirstOrDefault();
- }
+ if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
+ {
+ state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec)
+ ?? state.SupportedSubtitleCodecs.FirstOrDefault();
+ }
- var item = libraryManager.GetItemById(streamingRequest.Id);
+ var item = libraryManager.GetItemById(streamingRequest.Id);
- state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+ state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
- MediaSourceInfo? mediaSource = null;
- if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
- {
- var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
- ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
- : null;
+ MediaSourceInfo? mediaSource = null;
+ if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
+ {
+ var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
+ ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
+ : null;
- if (currentJob is not null)
- {
- mediaSource = currentJob.MediaSource;
- }
+ if (currentJob is not null)
+ {
+ mediaSource = currentJob.MediaSource;
+ }
- if (mediaSource is null)
- {
- var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
+ if (mediaSource is null)
+ {
+ var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
- mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
- ? mediaSources[0]
- : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
+ mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
+ ? mediaSources[0]
+ : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
- if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
- {
- mediaSource = mediaSources[0];
- }
+ if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
+ {
+ mediaSource = mediaSources[0];
}
}
- else
- {
- var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
- mediaSource = liveStreamInfo.Item1;
- state.DirectStreamProvider = liveStreamInfo.Item2;
- }
+ }
+ else
+ {
+ var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
+ mediaSource = liveStreamInfo.Item1;
+ state.DirectStreamProvider = liveStreamInfo.Item2;
+ }
- var encodingOptions = serverConfigurationManager.GetEncodingOptions();
+ var encodingOptions = serverConfigurationManager.GetEncodingOptions();
- encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
+ encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
- string? containerInternal = Path.GetExtension(state.RequestedUrl);
+ string? containerInternal = Path.GetExtension(state.RequestedUrl);
- if (!string.IsNullOrEmpty(streamingRequest.Container))
- {
- containerInternal = streamingRequest.Container;
- }
+ if (!string.IsNullOrEmpty(streamingRequest.Container))
+ {
+ containerInternal = streamingRequest.Container;
+ }
- if (string.IsNullOrEmpty(containerInternal))
- {
- containerInternal = streamingRequest.Static ?
- StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
- : GetOutputFileExtension(state, mediaSource);
- }
+ if (string.IsNullOrEmpty(containerInternal))
+ {
+ containerInternal = streamingRequest.Static ?
+ StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
+ : GetOutputFileExtension(state, mediaSource);
+ }
- state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
+ state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
- state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
- state.OutputAudioCodec = streamingRequest.AudioCodec;
+ state.OutputAudioCodec = streamingRequest.AudioCodec;
- state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
+ state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
- if (state.VideoRequest is not null)
- {
- state.OutputVideoCodec = state.Request.VideoCodec;
- state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
+ if (state.VideoRequest is not null)
+ {
+ state.OutputVideoCodec = state.Request.VideoCodec;
+ state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
- encodingHelper.TryStreamCopy(state);
+ encodingHelper.TryStreamCopy(state);
- if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
+ if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
+ {
+ var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
+ && !state.VideoRequest.Height.HasValue
+ && !state.VideoRequest.MaxWidth.HasValue
+ && !state.VideoRequest.MaxHeight.HasValue;
+
+ if (isVideoResolutionNotRequested
+ && state.VideoStream is not null
+ && state.VideoRequest.VideoBitRate.HasValue
+ && state.VideoStream.BitRate.HasValue
+ && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
{
- var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
- && !state.VideoRequest.Height.HasValue
- && !state.VideoRequest.MaxWidth.HasValue
- && !state.VideoRequest.MaxHeight.HasValue;
-
- if (isVideoResolutionNotRequested
- && state.VideoStream is not null
- && state.VideoRequest.VideoBitRate.HasValue
- && state.VideoStream.BitRate.HasValue
- && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
- {
- // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
- // and the requested video bitrate is higher than source video bitrate.
- if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
- {
- state.VideoRequest.MaxWidth = state.VideoStream?.Width;
- state.VideoRequest.MaxHeight = state.VideoStream?.Height;
- }
- }
- else
+ // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
+ // and the requested video bitrate is higher than source video bitrate.
+ if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
{
- var resolution = ResolutionNormalizer.Normalize(
- state.VideoStream?.BitRate,
- state.OutputVideoBitrate.Value,
- state.VideoRequest.MaxWidth,
- state.VideoRequest.MaxHeight);
-
- state.VideoRequest.MaxWidth = resolution.MaxWidth;
- state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ state.VideoRequest.MaxWidth = state.VideoStream?.Width;
+ state.VideoRequest.MaxHeight = state.VideoStream?.Height;
}
}
+ else
+ {
+ var resolution = ResolutionNormalizer.Normalize(
+ state.VideoStream?.BitRate,
+ state.OutputVideoBitrate.Value,
+ state.VideoRequest.MaxWidth,
+ state.VideoRequest.MaxHeight);
+
+ state.VideoRequest.MaxWidth = resolution.MaxWidth;
+ state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ }
}
+ }
- ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
+ ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
- var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
- ? GetOutputFileExtension(state, mediaSource)
- : ("." + state.OutputContainer);
+ var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
+ ? GetOutputFileExtension(state, mediaSource)
+ : ("." + state.OutputContainer);
- state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+ state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
- return state;
- }
+ return state;
+ }
- /// <summary>
- /// Adds the dlna headers.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- /// <param name="request">The <see cref="HttpRequest"/>.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public static void AddDlnaHeaders(
- StreamState state,
- IHeaderDictionary responseHeaders,
- bool isStaticallyStreamed,
- long? startTimeTicks,
- HttpRequest request,
- IDlnaManager dlnaManager)
+ /// <summary>
+ /// Adds the dlna headers.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="responseHeaders">The response headers.</param>
+ /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ /// <param name="request">The <see cref="HttpRequest"/>.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ public static void AddDlnaHeaders(
+ StreamState state,
+ IHeaderDictionary responseHeaders,
+ bool isStaticallyStreamed,
+ long? startTimeTicks,
+ HttpRequest request,
+ IDlnaManager dlnaManager)
+ {
+ if (!state.EnableDlnaHeaders)
{
- if (!state.EnableDlnaHeaders)
- {
- return;
- }
+ return;
+ }
- var profile = state.DeviceProfile;
+ var profile = state.DeviceProfile;
- StringValues transferMode = request.Headers["transferMode.dlna.org"];
- responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());
- responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");
+ StringValues transferMode = request.Headers["transferMode.dlna.org"];
+ responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());
+ responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");
- if (state.RunTimeTicks.HasValue)
+ if (state.RunTimeTicks.HasValue)
+ {
+ if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))
- {
- var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
- responseHeaders.Add("MediaInfo.sec", string.Format(
- CultureInfo.InvariantCulture,
- "SEC_Duration={0};",
- Convert.ToInt32(ms)));
- }
+ var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
+ responseHeaders.Add("MediaInfo.sec", string.Format(
+ CultureInfo.InvariantCulture,
+ "SEC_Duration={0};",
+ Convert.ToInt32(ms)));
+ }
- if (!isStaticallyStreamed && profile is not null)
- {
- AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
- }
+ if (!isStaticallyStreamed && profile is not null)
+ {
+ AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
}
+ }
- profile ??= dlnaManager.GetDefaultProfile();
+ profile ??= dlnaManager.GetDefaultProfile();
- var audioCodec = state.ActualOutputAudioCodec;
+ var audioCodec = state.ActualOutputAudioCodec;
- if (!state.IsVideoRequest)
- {
- responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(
- profile,
- state.OutputContainer,
- audioCodec,
- state.OutputAudioBitrate,
- state.OutputAudioSampleRate,
- state.OutputAudioChannels,
- state.OutputAudioBitDepth,
- isStaticallyStreamed,
- state.RunTimeTicks,
- state.TranscodeSeekInfo));
- }
- else
- {
- var videoCodec = state.ActualOutputVideoCodec;
+ if (!state.IsVideoRequest)
+ {
+ responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(
+ profile,
+ state.OutputContainer,
+ audioCodec,
+ state.OutputAudioBitrate,
+ state.OutputAudioSampleRate,
+ state.OutputAudioChannels,
+ state.OutputAudioBitDepth,
+ isStaticallyStreamed,
+ state.RunTimeTicks,
+ state.TranscodeSeekInfo));
+ }
+ else
+ {
+ var videoCodec = state.ActualOutputVideoCodec;
- responseHeaders.Add(
- "contentFeatures.dlna.org",
- ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
- }
+ responseHeaders.Add(
+ "contentFeatures.dlna.org",
+ ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
}
+ }
- /// <summary>
- /// Parses the time seek header.
- /// </summary>
- /// <param name="value">The time seek header string.</param>
- /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
- private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)
+ /// <summary>
+ /// Parses the time seek header.
+ /// </summary>
+ /// <param name="value">The time seek header string.</param>
+ /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
+ private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)
+ {
+ if (value.IsEmpty)
{
- if (value.IsEmpty)
- {
- return null;
- }
+ return null;
+ }
- const string npt = "npt=";
- if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException("Invalid timeseek header");
- }
+ const string npt = "npt=";
+ if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException("Invalid timeseek header");
+ }
- var index = value.IndexOf('-');
- value = index == -1
- ? value.Slice(npt.Length)
- : value.Slice(npt.Length, index - npt.Length);
- if (value.IndexOf(':') == -1)
+ var index = value.IndexOf('-');
+ value = index == -1
+ ? value.Slice(npt.Length)
+ : value.Slice(npt.Length, index - npt.Length);
+ if (!value.Contains(':'))
+ {
+ // Parses npt times in the format of '417.33'
+ if (double.TryParse(value, CultureInfo.InvariantCulture, out var seconds))
{
- // Parses npt times in the format of '417.33'
- if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
- {
- return TimeSpan.FromSeconds(seconds).Ticks;
- }
-
- throw new ArgumentException("Invalid timeseek header");
+ return TimeSpan.FromSeconds(seconds).Ticks;
}
- try
- {
- // Parses npt times in the format of '10:19:25.7'
- return TimeSpan.Parse(value, CultureInfo.InvariantCulture).Ticks;
- }
- catch
- {
- throw new ArgumentException("Invalid timeseek header");
- }
+ throw new ArgumentException("Invalid timeseek header");
}
- /// <summary>
- /// Parses query parameters as StreamOptions.
- /// </summary>
- /// <param name="queryString">The query string.</param>
- /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
- private static Dictionary<string, string?> ParseStreamOptions(IQueryCollection queryString)
+ try
+ {
+ // Parses npt times in the format of '10:19:25.7'
+ return TimeSpan.Parse(value, CultureInfo.InvariantCulture).Ticks;
+ }
+ catch
+ {
+ throw new ArgumentException("Invalid timeseek header");
+ }
+ }
+
+ /// <summary>
+ /// Parses query parameters as StreamOptions.
+ /// </summary>
+ /// <param name="queryString">The query string.</param>
+ /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
+ private static Dictionary<string, string?> ParseStreamOptions(IQueryCollection queryString)
+ {
+ Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
+ foreach (var param in queryString)
{
- Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
- foreach (var param in queryString)
+ if (char.IsLower(param.Key[0]))
{
- if (char.IsLower(param.Key[0]))
- {
- // This was probably not parsed initially and should be a StreamOptions
- // or the generated URL should correctly serialize it
- // TODO: This should be incorporated either in the lower framework for parsing requests
- streamOptions[param.Key] = param.Value;
- }
+ // This was probably not parsed initially and should be a StreamOptions
+ // or the generated URL should correctly serialize it
+ // TODO: This should be incorporated either in the lower framework for parsing requests
+ streamOptions[param.Key] = param.Value;
}
-
- return streamOptions;
}
- /// <summary>
- /// Adds the dlna time seek headers to the response.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
- {
- var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
- var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+ return streamOptions;
+ }
- responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
- CultureInfo.InvariantCulture,
- "npt={0}-{1}/{1}",
- startSeconds,
- runtimeSeconds));
- responseHeaders.Add("X-AvailableSeekRange", string.Format(
- CultureInfo.InvariantCulture,
- "1 npt={0}-{1}",
- startSeconds,
- runtimeSeconds));
+ /// <summary>
+ /// Adds the dlna time seek headers to the response.
+ /// </summary>
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
+ /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
+ {
+ var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+ var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+
+ responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
+ CultureInfo.InvariantCulture,
+ "npt={0}-{1}/{1}",
+ startSeconds,
+ runtimeSeconds));
+ responseHeaders.Add("X-AvailableSeekRange", string.Format(
+ CultureInfo.InvariantCulture,
+ "1 npt={0}-{1}",
+ startSeconds,
+ runtimeSeconds));
+ }
+
+ /// <summary>
+ /// Gets the output file extension.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <returns>System.String.</returns>
+ private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+ {
+ var ext = Path.GetExtension(state.RequestedUrl);
+
+ if (!string.IsNullOrEmpty(ext))
+ {
+ return ext;
}
- /// <summary>
- /// Gets the output file extension.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="mediaSource">The mediaSource.</param>
- /// <returns>System.String.</returns>
- private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+ // Try to infer based on the desired video codec
+ if (state.IsVideoRequest)
{
- var ext = Path.GetExtension(state.RequestedUrl);
+ var videoCodec = state.Request.VideoCodec;
- if (!string.IsNullOrEmpty(ext))
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return ext;
+ return ".ts";
}
- // Try to infer based on the desired video codec
- if (state.IsVideoRequest)
+ if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
{
- var videoCodec = state.Request.VideoCodec;
-
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- return ".ts";
- }
-
- if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
- {
- return ".ogv";
- }
-
- if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
- {
- return ".webm";
- }
-
- if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
- {
- return ".asf";
- }
+ return ".ogv";
}
- // Try to infer based on the desired audio codec
- if (!state.IsVideoRequest)
+ if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
{
- var audioCodec = state.Request.AudioCodec;
+ return ".webm";
+ }
- if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".aac";
- }
+ if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".asf";
+ }
+ }
- if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".mp3";
- }
+ // Try to infer based on the desired audio codec
+ if (!state.IsVideoRequest)
+ {
+ var audioCodec = state.Request.AudioCodec;
- if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".ogg";
- }
+ if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".aac";
+ }
- if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".wma";
- }
+ if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".mp3";
}
- // Fallback to the container of mediaSource
- if (!string.IsNullOrEmpty(mediaSource?.Container))
+ if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
{
- var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
- return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
+ return ".ogg";
}
- return null;
+ if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".wma";
+ }
}
- /// <summary>
- /// Gets the output file path for transcoding.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="outputFileExtension">The file extension of the output file.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="deviceId">The device id.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
- private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
+ // Fallback to the container of mediaSource
+ if (!string.IsNullOrEmpty(mediaSource?.Container))
{
- var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
+ var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
+ return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
+ }
- var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var ext = outputFileExtension?.ToLowerInvariant();
- var folder = serverConfigurationManager.GetTranscodePath();
+ return null;
+ }
- return Path.Combine(folder, filename + ext);
- }
+ /// <summary>
+ /// Gets the output file path for transcoding.
+ /// </summary>
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
+ /// <param name="outputFileExtension">The file extension of the output file.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
+ private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
+ {
+ var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
- private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
- {
- if (!string.IsNullOrWhiteSpace(deviceProfileId))
- {
- state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
+ var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ var ext = outputFileExtension?.ToLowerInvariant();
+ var folder = serverConfigurationManager.GetTranscodePath();
- if (state.DeviceProfile is null)
- {
- var caps = deviceManager.GetCapabilities(deviceProfileId);
- state.DeviceProfile = caps is null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
- }
- }
+ return Path.Combine(folder, filename + ext);
+ }
- var profile = state.DeviceProfile;
+ private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
+ {
+ if (!string.IsNullOrWhiteSpace(deviceProfileId))
+ {
+ state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
- if (profile is null)
+ if (state.DeviceProfile is null)
{
- // Don't use settings from the default profile.
- // Only use a specific profile if it was requested.
- return;
+ var caps = deviceManager.GetCapabilities(deviceProfileId);
+ state.DeviceProfile = caps is null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
}
+ }
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
+ var profile = state.DeviceProfile;
- var mediaProfile = !state.IsVideoRequest
- ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
- : profile.GetVideoMediaProfile(
- state.OutputContainer,
- audioCodec,
- videoCodec,
- state.OutputWidth,
- state.OutputHeight,
- state.TargetVideoBitDepth,
- state.OutputVideoBitrate,
- state.TargetVideoProfile,
- state.TargetVideoRangeType,
- state.TargetVideoLevel,
- state.TargetFramerate,
- state.TargetPacketLength,
- state.TargetTimestamp,
- state.IsTargetAnamorphic,
- state.IsTargetInterlaced,
- state.TargetRefFrames,
- state.TargetVideoStreamCount,
- state.TargetAudioStreamCount,
- state.TargetVideoCodecTag,
- state.IsTargetAVC);
-
- if (mediaProfile is not null)
- {
- state.MimeType = mediaProfile.MimeType;
- }
+ if (profile is null)
+ {
+ // Don't use settings from the default profile.
+ // Only use a specific profile if it was requested.
+ return;
+ }
- if (!(@static.HasValue && @static.Value))
+ var audioCodec = state.ActualOutputAudioCodec;
+ var videoCodec = state.ActualOutputVideoCodec;
+
+ var mediaProfile = !state.IsVideoRequest
+ ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
+ : profile.GetVideoMediaProfile(
+ state.OutputContainer,
+ audioCodec,
+ videoCodec,
+ state.OutputWidth,
+ state.OutputHeight,
+ state.TargetVideoBitDepth,
+ state.OutputVideoBitrate,
+ state.TargetVideoProfile,
+ state.TargetVideoRangeType,
+ state.TargetVideoLevel,
+ state.TargetFramerate,
+ state.TargetPacketLength,
+ state.TargetTimestamp,
+ state.IsTargetAnamorphic,
+ state.IsTargetInterlaced,
+ state.TargetRefFrames,
+ state.TargetVideoStreamCount,
+ state.TargetAudioStreamCount,
+ state.TargetVideoCodecTag,
+ state.IsTargetAVC);
+
+ if (mediaProfile is not null)
+ {
+ state.MimeType = mediaProfile.MimeType;
+ }
+
+ if (!(@static.HasValue && @static.Value))
+ {
+ var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
+
+ if (transcodingProfile is not null)
{
- var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
+ state.EstimateContentLength = transcodingProfile.EstimateContentLength;
+ // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
+ state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
- if (transcodingProfile is not null)
+ if (state.VideoRequest is not null)
{
- state.EstimateContentLength = transcodingProfile.EstimateContentLength;
- // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
- state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
-
- if (state.VideoRequest is not null)
- {
- state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
- state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
- }
+ state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
+ state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
}
}
}
+ }
- /// <summary>
- /// Parses the parameters.
- /// </summary>
- /// <param name="request">The request.</param>
- private static void ParseParams(StreamingRequestDto request)
+ /// <summary>
+ /// Parses the parameters.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ private static void ParseParams(StreamingRequestDto request)
+ {
+ if (string.IsNullOrEmpty(request.Params))
{
- if (string.IsNullOrEmpty(request.Params))
- {
- return;
- }
+ return;
+ }
- var vals = request.Params.Split(';');
+ var vals = request.Params.Split(';');
- var videoRequest = request as VideoRequestDto;
+ var videoRequest = request as VideoRequestDto;
- for (var i = 0; i < vals.Length; i++)
- {
- var val = vals[i];
+ for (var i = 0; i < vals.Length; i++)
+ {
+ var val = vals[i];
- if (string.IsNullOrWhiteSpace(val))
- {
- continue;
- }
+ if (string.IsNullOrWhiteSpace(val))
+ {
+ continue;
+ }
- switch (i)
- {
- case 0:
- request.DeviceProfileId = val;
- break;
- case 1:
- request.DeviceId = val;
- break;
- case 2:
- request.MediaSourceId = val;
- break;
- case 3:
- request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- break;
- case 4:
- if (videoRequest is not null)
- {
- videoRequest.VideoCodec = val;
- }
+ switch (i)
+ {
+ case 0:
+ request.DeviceProfileId = val;
+ break;
+ case 1:
+ request.DeviceId = val;
+ break;
+ case 2:
+ request.MediaSourceId = val;
+ break;
+ case 3:
+ request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ break;
+ case 4:
+ if (videoRequest is not null)
+ {
+ videoRequest.VideoCodec = val;
+ }
- break;
- case 5:
- request.AudioCodec = val;
- break;
- case 6:
- if (videoRequest is not null)
- {
- videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 5:
+ request.AudioCodec = val;
+ break;
+ case 6:
+ if (videoRequest is not null)
+ {
+ videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 7:
- if (videoRequest is not null)
- {
- videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 7:
+ if (videoRequest is not null)
+ {
+ videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 8:
- if (videoRequest is not null)
- {
- videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 8:
+ if (videoRequest is not null)
+ {
+ videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 9:
- request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 10:
- request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 11:
- if (videoRequest is not null)
- {
- videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 9:
+ request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 10:
+ request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 11:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 12:
- if (videoRequest is not null)
- {
- videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 12:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 13:
- if (videoRequest is not null)
- {
- videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 13:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 14:
- request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 15:
- if (videoRequest is not null)
- {
- videoRequest.Level = val;
- }
+ break;
+ case 14:
+ request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 15:
+ if (videoRequest is not null)
+ {
+ videoRequest.Level = val;
+ }
- break;
- case 16:
- if (videoRequest is not null)
- {
- videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 16:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 17:
- if (videoRequest is not null)
- {
- videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 17:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 18:
- if (videoRequest is not null)
- {
- videoRequest.Profile = val;
- }
+ break;
+ case 18:
+ if (videoRequest is not null)
+ {
+ videoRequest.Profile = val;
+ }
- break;
- case 19:
- // cabac no longer used
- break;
- case 20:
- request.PlaySessionId = val;
- break;
- case 21:
- // api_key
- break;
- case 22:
- request.LiveStreamId = val;
- break;
- case 23:
- // Duplicating ItemId because of MediaMonkey
- break;
- case 24:
- if (videoRequest is not null)
- {
- videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 19:
+ // cabac no longer used
+ break;
+ case 20:
+ request.PlaySessionId = val;
+ break;
+ case 21:
+ // api_key
+ break;
+ case 22:
+ request.LiveStreamId = val;
+ break;
+ case 23:
+ // Duplicating ItemId because of MediaMonkey
+ break;
+ case 24:
+ if (videoRequest is not null)
+ {
+ videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 25:
- if (!string.IsNullOrWhiteSpace(val) && videoRequest is not null)
+ break;
+ case 25:
+ if (!string.IsNullOrWhiteSpace(val) && videoRequest is not null)
+ {
+ if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
{
- if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
- {
- videoRequest.SubtitleMethod = method;
- }
+ videoRequest.SubtitleMethod = method;
}
+ }
- break;
- case 26:
- request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 27:
- if (videoRequest is not null)
- {
- videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 26:
+ request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 27:
+ if (videoRequest is not null)
+ {
+ videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 28:
- request.Tag = val;
- break;
- case 29:
- if (videoRequest is not null)
- {
- videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 28:
+ request.Tag = val;
+ break;
+ case 29:
+ if (videoRequest is not null)
+ {
+ videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 30:
- request.SubtitleCodec = val;
- break;
- case 31:
- if (videoRequest is not null)
- {
- videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 30:
+ request.SubtitleCodec = val;
+ break;
+ case 31:
+ if (videoRequest is not null)
+ {
+ videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 32:
- if (videoRequest is not null)
- {
- videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 32:
+ if (videoRequest is not null)
+ {
+ videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 33:
- request.TranscodeReasons = val;
- break;
- }
+ break;
+ case 33:
+ request.TranscodeReasons = val;
+ break;
}
}
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 77dd51860..f25a71869 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -27,888 +27,898 @@ using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Transcoding job helpers.
+/// </summary>
+public class TranscodingJobHelper : IDisposable
{
/// <summary>
- /// Transcoding job helpers.
+ /// The active transcoding jobs.
+ /// </summary>
+ private static readonly List<TranscodingJobDto> _activeTranscodingJobs = new List<TranscodingJobDto>();
+
+ /// <summary>
+ /// The transcoding locks.
/// </summary>
- public class TranscodingJobHelper : IDisposable
+ private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
+
+ private readonly IAttachmentExtractor _attachmentExtractor;
+ private readonly IApplicationPaths _appPaths;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<TranscodingJobHelper> _logger;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
+ /// </summary>
+ /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public TranscodingJobHelper(
+ IAttachmentExtractor attachmentExtractor,
+ IApplicationPaths appPaths,
+ ILogger<TranscodingJobHelper> logger,
+ IMediaSourceManager mediaSourceManager,
+ IFileSystem fileSystem,
+ IMediaEncoder mediaEncoder,
+ IServerConfigurationManager serverConfigurationManager,
+ ISessionManager sessionManager,
+ EncodingHelper encodingHelper,
+ ILoggerFactory loggerFactory,
+ IUserManager userManager)
{
- /// <summary>
- /// The active transcoding jobs.
- /// </summary>
- private static readonly List<TranscodingJobDto> _activeTranscodingJobs = new List<TranscodingJobDto>();
-
- /// <summary>
- /// The transcoding locks.
- /// </summary>
- private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
-
- private readonly IAttachmentExtractor _attachmentExtractor;
- private readonly IApplicationPaths _appPaths;
- private readonly EncodingHelper _encodingHelper;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<TranscodingJobHelper> _logger;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly ISessionManager _sessionManager;
- private readonly ILoggerFactory _loggerFactory;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
- /// </summary>
- /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
- /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public TranscodingJobHelper(
- IAttachmentExtractor attachmentExtractor,
- IApplicationPaths appPaths,
- ILogger<TranscodingJobHelper> logger,
- IMediaSourceManager mediaSourceManager,
- IFileSystem fileSystem,
- IMediaEncoder mediaEncoder,
- IServerConfigurationManager serverConfigurationManager,
- ISessionManager sessionManager,
- EncodingHelper encodingHelper,
- ILoggerFactory loggerFactory,
- IUserManager userManager)
- {
- _attachmentExtractor = attachmentExtractor;
- _appPaths = appPaths;
- _logger = logger;
- _mediaSourceManager = mediaSourceManager;
- _fileSystem = fileSystem;
- _mediaEncoder = mediaEncoder;
- _serverConfigurationManager = serverConfigurationManager;
- _sessionManager = sessionManager;
- _encodingHelper = encodingHelper;
- _loggerFactory = loggerFactory;
- _userManager = userManager;
-
- DeleteEncodedMediaCache();
-
- sessionManager.PlaybackProgress += OnPlaybackProgress;
- sessionManager.PlaybackStart += OnPlaybackProgress;
- }
-
- /// <summary>
- /// Get transcoding job.
- /// </summary>
- /// <param name="playSessionId">Playback session id.</param>
- /// <returns>The transcoding job.</returns>
- public TranscodingJobDto? GetTranscodingJob(string playSessionId)
- {
- lock (_activeTranscodingJobs)
- {
- return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
- }
- }
+ _attachmentExtractor = attachmentExtractor;
+ _appPaths = appPaths;
+ _logger = logger;
+ _mediaSourceManager = mediaSourceManager;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _serverConfigurationManager = serverConfigurationManager;
+ _sessionManager = sessionManager;
+ _encodingHelper = encodingHelper;
+ _loggerFactory = loggerFactory;
+ _userManager = userManager;
+
+ DeleteEncodedMediaCache();
+
+ sessionManager.PlaybackProgress += OnPlaybackProgress;
+ sessionManager.PlaybackStart += OnPlaybackProgress;
+ }
- /// <summary>
- /// Get transcoding job.
- /// </summary>
- /// <param name="path">Path to the transcoding file.</param>
- /// <param name="type">The <see cref="TranscodingJobType"/>.</param>
- /// <returns>The transcoding job.</returns>
- public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type)
+ /// <summary>
+ /// Get transcoding job.
+ /// </summary>
+ /// <param name="playSessionId">Playback session id.</param>
+ /// <returns>The transcoding job.</returns>
+ public TranscodingJobDto? GetTranscodingJob(string playSessionId)
+ {
+ lock (_activeTranscodingJobs)
{
- lock (_activeTranscodingJobs)
- {
- return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
- }
+ return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
}
+ }
- /// <summary>
- /// Ping transcoding job.
- /// </summary>
- /// <param name="playSessionId">Play session id.</param>
- /// <param name="isUserPaused">Is user paused.</param>
- /// <exception cref="ArgumentNullException">Play session id is null.</exception>
- public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
+ /// <summary>
+ /// Get transcoding job.
+ /// </summary>
+ /// <param name="path">Path to the transcoding file.</param>
+ /// <param name="type">The <see cref="TranscodingJobType"/>.</param>
+ /// <returns>The transcoding job.</returns>
+ public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type)
+ {
+ lock (_activeTranscodingJobs)
{
- ArgumentException.ThrowIfNullOrEmpty(playSessionId);
+ return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+ }
+ }
- _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
+ /// <summary>
+ /// Ping transcoding job.
+ /// </summary>
+ /// <param name="playSessionId">Play session id.</param>
+ /// <param name="isUserPaused">Is user paused.</param>
+ /// <exception cref="ArgumentNullException">Play session id is null.</exception>
+ public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(playSessionId);
- List<TranscodingJobDto> jobs;
+ _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
- lock (_activeTranscodingJobs)
+ List<TranscodingJobDto> jobs;
+
+ lock (_activeTranscodingJobs)
+ {
+ // This is really only needed for HLS.
+ // Progressive streams can stop on their own reliably.
+ jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+
+ foreach (var job in jobs)
+ {
+ if (isUserPaused.HasValue)
{
- // This is really only needed for HLS.
- // Progressive streams can stop on their own reliably.
- jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
+ _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
+ job.IsUserPaused = isUserPaused.Value;
}
- foreach (var job in jobs)
- {
- if (isUserPaused.HasValue)
- {
- _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
- job.IsUserPaused = isUserPaused.Value;
- }
+ PingTimer(job, true);
+ }
+ }
- PingTimer(job, true);
- }
+ private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn)
+ {
+ if (job.HasExited)
+ {
+ job.StopKillTimer();
+ return;
}
- private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn)
+ var timerDuration = 10000;
+
+ if (job.Type != TranscodingJobType.Progressive)
{
- if (job.HasExited)
- {
- job.StopKillTimer();
- return;
- }
+ timerDuration = 60000;
+ }
- var timerDuration = 10000;
+ job.PingTimeout = timerDuration;
+ job.LastPingDate = DateTime.UtcNow;
- if (job.Type != TranscodingJobType.Progressive)
- {
- timerDuration = 60000;
- }
+ // Don't start the timer for playback checkins with progressive streaming
+ if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
+ {
+ job.StartKillTimer(OnTranscodeKillTimerStopped);
+ }
+ else
+ {
+ job.ChangeKillTimerIfStarted();
+ }
+ }
- job.PingTimeout = timerDuration;
- job.LastPingDate = DateTime.UtcNow;
+ /// <summary>
+ /// Called when [transcode kill timer stopped].
+ /// </summary>
+ /// <param name="state">The state.</param>
+ private async void OnTranscodeKillTimerStopped(object? state)
+ {
+ var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state));
+ if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
+ {
+ var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
- // Don't start the timer for playback checkins with progressive streaming
- if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
+ if (timeSinceLastPing < job.PingTimeout)
{
- job.StartKillTimer(OnTranscodeKillTimerStopped);
- }
- else
- {
- job.ChangeKillTimerIfStarted();
+ job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
+ return;
}
}
- /// <summary>
- /// Called when [transcode kill timer stopped].
- /// </summary>
- /// <param name="state">The state.</param>
- private async void OnTranscodeKillTimerStopped(object? state)
- {
- var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state));
- if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
- {
- var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
+ _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
- if (timeSinceLastPing < job.PingTimeout)
- {
- job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
- return;
- }
- }
+ await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
+ }
- _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+ /// <summary>
+ /// Kills the single transcoding job.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="playSessionId">The play session identifier.</param>
+ /// <param name="deleteFiles">The delete files.</param>
+ /// <returns>Task.</returns>
+ public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
+ {
+ return KillTranscodingJobs(
+ j => string.IsNullOrWhiteSpace(playSessionId)
+ ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
+ : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase),
+ deleteFiles);
+ }
- await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Kills the transcoding jobs.
+ /// </summary>
+ /// <param name="killJob">The kill job.</param>
+ /// <param name="deleteFiles">The delete files.</param>
+ /// <returns>Task.</returns>
+ private Task KillTranscodingJobs(Func<TranscodingJobDto, bool> killJob, Func<string, bool> deleteFiles)
+ {
+ var jobs = new List<TranscodingJobDto>();
- /// <summary>
- /// Kills the single transcoding job.
- /// </summary>
- /// <param name="deviceId">The device id.</param>
- /// <param name="playSessionId">The play session identifier.</param>
- /// <param name="deleteFiles">The delete files.</param>
- /// <returns>Task.</returns>
- public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
+ lock (_activeTranscodingJobs)
{
- return KillTranscodingJobs(
- j => string.IsNullOrWhiteSpace(playSessionId)
- ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
- : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase),
- deleteFiles);
+ // This is really only needed for HLS.
+ // Progressive streams can stop on their own reliably.
+ jobs.AddRange(_activeTranscodingJobs.Where(killJob));
}
- /// <summary>
- /// Kills the transcoding jobs.
- /// </summary>
- /// <param name="killJob">The kill job.</param>
- /// <param name="deleteFiles">The delete files.</param>
- /// <returns>Task.</returns>
- private Task KillTranscodingJobs(Func<TranscodingJobDto, bool> killJob, Func<string, bool> deleteFiles)
+ if (jobs.Count == 0)
{
- var jobs = new List<TranscodingJobDto>();
+ return Task.CompletedTask;
+ }
- lock (_activeTranscodingJobs)
+ IEnumerable<Task> GetKillJobs()
+ {
+ foreach (var job in jobs)
{
- // This is really only needed for HLS.
- // Progressive streams can stop on their own reliably.
- jobs.AddRange(_activeTranscodingJobs.Where(killJob));
+ yield return KillTranscodingJob(job, false, deleteFiles);
}
+ }
- if (jobs.Count == 0)
- {
- return Task.CompletedTask;
- }
+ return Task.WhenAll(GetKillJobs());
+ }
- IEnumerable<Task> GetKillJobs()
- {
- foreach (var job in jobs)
- {
- yield return KillTranscodingJob(job, false, deleteFiles);
- }
- }
+ /// <summary>
+ /// Kills the transcoding job.
+ /// </summary>
+ /// <param name="job">The job.</param>
+ /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
+ /// <param name="delete">The delete.</param>
+ private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func<string, bool> delete)
+ {
+ job.DisposeKillTimer();
- return Task.WhenAll(GetKillJobs());
- }
+ _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
- /// <summary>
- /// Kills the transcoding job.
- /// </summary>
- /// <param name="job">The job.</param>
- /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
- /// <param name="delete">The delete.</param>
- private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func<string, bool> delete)
+ lock (_activeTranscodingJobs)
{
- job.DisposeKillTimer();
-
- _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+ _activeTranscodingJobs.Remove(job);
- lock (_activeTranscodingJobs)
+ if (job.CancellationTokenSource?.IsCancellationRequested == false)
{
- _activeTranscodingJobs.Remove(job);
-
- if (job.CancellationTokenSource?.IsCancellationRequested == false)
- {
- job.CancellationTokenSource.Cancel();
- }
+ job.CancellationTokenSource.Cancel();
}
+ }
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(job.Path!);
- }
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(job.Path!);
+ }
- lock (job.ProcessLock!)
- {
- #pragma warning disable CA1849 // Can't await in lock block
- job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
+ lock (job.ProcessLock!)
+ {
+#pragma warning disable CA1849 // Can't await in lock block
+ job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
- var process = job.Process;
+ var process = job.Process;
- var hasExited = job.HasExited;
+ var hasExited = job.HasExited;
- if (!hasExited)
+ if (!hasExited)
+ {
+ try
{
- try
- {
- _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
+ _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
- process!.StandardInput.WriteLine("q");
+ process!.StandardInput.WriteLine("q");
- // Need to wait because killing is asynchronous.
- if (!process.WaitForExit(5000))
- {
- _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
- process.Kill();
- }
- }
- catch (InvalidOperationException)
+ // Need to wait because killing is asynchronous.
+ if (!process.WaitForExit(5000))
{
+ _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
+ process.Kill();
}
}
- #pragma warning restore CA1849
- }
-
- if (delete(job.Path!))
- {
- await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+ catch (InvalidOperationException)
+ {
+ }
}
+#pragma warning restore CA1849
+ }
- if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
+ if (delete(job.Path!))
+ {
+ await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+ if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay)
{
- try
+ var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat");
+ if (File.Exists(concatFilePath))
{
- await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
+ _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath);
+ File.Delete(concatFilePath);
}
}
}
- private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
+ if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
{
- if (retryCount >= 10)
+ try
{
- return;
+ await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
}
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
+ }
+ }
+ }
- _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
+ private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
+ {
+ if (retryCount >= 10)
+ {
+ return;
+ }
- await Task.Delay(delayMs).ConfigureAwait(false);
+ _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
- try
- {
- if (jobType == TranscodingJobType.Progressive)
- {
- DeleteProgressivePartialStreamFiles(path);
- }
- else
- {
- DeleteHlsPartialStreamFiles(path);
- }
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+ await Task.Delay(delayMs).ConfigureAwait(false);
- await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
+ try
+ {
+ if (jobType == TranscodingJobType.Progressive)
+ {
+ DeleteProgressivePartialStreamFiles(path);
}
- catch (Exception ex)
+ else
{
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+ DeleteHlsPartialStreamFiles(path);
}
}
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- /// <summary>
- /// Deletes the progressive partial stream files.
- /// </summary>
- /// <param name="outputFilePath">The output file path.</param>
- private void DeleteProgressivePartialStreamFiles(string outputFilePath)
+ await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
+ }
+ catch (Exception ex)
{
- if (File.Exists(outputFilePath))
- {
- _fileSystem.DeleteFile(outputFilePath);
- }
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
}
+ }
- /// <summary>
- /// Deletes the HLS partial stream files.
- /// </summary>
- /// <param name="outputFilePath">The output file path.</param>
- private void DeleteHlsPartialStreamFiles(string outputFilePath)
+ /// <summary>
+ /// Deletes the progressive partial stream files.
+ /// </summary>
+ /// <param name="outputFilePath">The output file path.</param>
+ private void DeleteProgressivePartialStreamFiles(string outputFilePath)
+ {
+ if (File.Exists(outputFilePath))
{
- var directory = Path.GetDirectoryName(outputFilePath)
- ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+ _fileSystem.DeleteFile(outputFilePath);
+ }
+ }
- var name = Path.GetFileNameWithoutExtension(outputFilePath);
+ /// <summary>
+ /// Deletes the HLS partial stream files.
+ /// </summary>
+ /// <param name="outputFilePath">The output file path.</param>
+ private void DeleteHlsPartialStreamFiles(string outputFilePath)
+ {
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+
+ var name = Path.GetFileNameWithoutExtension(outputFilePath);
- var filesToDelete = _fileSystem.GetFilePaths(directory)
- .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
+ var filesToDelete = _fileSystem.GetFilePaths(directory)
+ .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
- List<Exception>? exs = null;
- foreach (var file in filesToDelete)
+ List<Exception>? exs = null;
+ foreach (var file in filesToDelete)
+ {
+ try
{
- try
- {
- _logger.LogDebug("Deleting HLS file {0}", file);
- _fileSystem.DeleteFile(file);
- }
- catch (IOException ex)
- {
- (exs ??= new List<Exception>(4)).Add(ex);
- _logger.LogError(ex, "Error deleting HLS file {Path}", file);
- }
+ _logger.LogDebug("Deleting HLS file {0}", file);
+ _fileSystem.DeleteFile(file);
}
-
- if (exs is not null)
+ catch (IOException ex)
{
- throw new AggregateException("Error deleting HLS files", exs);
+ (exs ??= new List<Exception>(4)).Add(ex);
+ _logger.LogError(ex, "Error deleting HLS file {Path}", file);
}
}
- /// <summary>
- /// Report the transcoding progress to the session manager.
- /// </summary>
- /// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param>
- /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
- /// <param name="transcodingPosition">The current transcoding position.</param>
- /// <param name="framerate">The framerate of the transcoding job.</param>
- /// <param name="percentComplete">The completion percentage of the transcode.</param>
- /// <param name="bytesTranscoded">The number of bytes transcoded.</param>
- /// <param name="bitRate">The bitrate of the transcoding job.</param>
- public void ReportTranscodingProgress(
- TranscodingJobDto job,
- StreamState state,
- TimeSpan? transcodingPosition,
- float? framerate,
- double? percentComplete,
- long? bytesTranscoded,
- int? bitRate)
- {
- var ticks = transcodingPosition?.Ticks;
+ if (exs is not null)
+ {
+ throw new AggregateException("Error deleting HLS files", exs);
+ }
+ }
- if (job is not null)
- {
- job.Framerate = framerate;
- job.CompletionPercentage = percentComplete;
- job.TranscodingPositionTicks = ticks;
- job.BytesTranscoded = bytesTranscoded;
- job.BitRate = bitRate;
- }
+ /// <summary>
+ /// Report the transcoding progress to the session manager.
+ /// </summary>
+ /// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param>
+ /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
+ /// <param name="transcodingPosition">The current transcoding position.</param>
+ /// <param name="framerate">The framerate of the transcoding job.</param>
+ /// <param name="percentComplete">The completion percentage of the transcode.</param>
+ /// <param name="bytesTranscoded">The number of bytes transcoded.</param>
+ /// <param name="bitRate">The bitrate of the transcoding job.</param>
+ public void ReportTranscodingProgress(
+ TranscodingJobDto job,
+ StreamState state,
+ TimeSpan? transcodingPosition,
+ float? framerate,
+ double? percentComplete,
+ long? bytesTranscoded,
+ int? bitRate)
+ {
+ var ticks = transcodingPosition?.Ticks;
- var deviceId = state.Request.DeviceId;
+ if (job is not null)
+ {
+ job.Framerate = framerate;
+ job.CompletionPercentage = percentComplete;
+ job.TranscodingPositionTicks = ticks;
+ job.BytesTranscoded = bytesTranscoded;
+ job.BitRate = bitRate;
+ }
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
- var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
- HardwareEncodingType? hardwareAccelerationType = null;
- if (!string.IsNullOrEmpty(hardwareAccelerationTypeString)
- && Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
- {
- hardwareAccelerationType = parsedHardwareAccelerationType;
- }
+ var deviceId = state.Request.DeviceId;
- _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
- {
- Bitrate = bitRate ?? state.TotalOutputBitrate,
- AudioCodec = audioCodec,
- VideoCodec = videoCodec,
- Container = state.OutputContainer,
- Framerate = framerate,
- CompletionPercentage = percentComplete,
- Width = state.OutputWidth,
- Height = state.OutputHeight,
- AudioChannels = state.OutputAudioChannels,
- IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
- IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
- HardwareAccelerationType = hardwareAccelerationType,
- TranscodeReasons = state.TranscodeReasons
- });
- }
+ if (!string.IsNullOrWhiteSpace(deviceId))
+ {
+ var audioCodec = state.ActualOutputAudioCodec;
+ var videoCodec = state.ActualOutputVideoCodec;
+ var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
+ HardwareEncodingType? hardwareAccelerationType = null;
+ if (Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
+ {
+ hardwareAccelerationType = parsedHardwareAccelerationType;
+ }
+
+ _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
+ {
+ Bitrate = bitRate ?? state.TotalOutputBitrate,
+ AudioCodec = audioCodec,
+ VideoCodec = videoCodec,
+ Container = state.OutputContainer,
+ Framerate = framerate,
+ CompletionPercentage = percentComplete,
+ Width = state.OutputWidth,
+ Height = state.OutputHeight,
+ AudioChannels = state.OutputAudioChannels,
+ IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
+ IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+ HardwareAccelerationType = hardwareAccelerationType,
+ TranscodeReasons = state.TranscodeReasons
+ });
}
+ }
- /// <summary>
- /// Starts FFmpeg.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="outputPath">The output path.</param>
- /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
- /// <param name="request">The <see cref="HttpRequest"/>.</param>
- /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <param name="workingDirectory">The working directory.</param>
- /// <returns>Task.</returns>
- public async Task<TranscodingJobDto> StartFfMpeg(
- StreamState state,
- string outputPath,
- string commandLineArguments,
- HttpRequest request,
- TranscodingJobType transcodingJobType,
- CancellationTokenSource cancellationTokenSource,
- string? workingDirectory = null)
- {
- var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
- Directory.CreateDirectory(directory);
-
- await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
-
- if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ /// <summary>
+ /// Starts FFmpeg.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
+ /// <param name="request">The <see cref="HttpRequest"/>.</param>
+ /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+ /// <param name="cancellationTokenSource">The cancellation token source.</param>
+ /// <param name="workingDirectory">The working directory.</param>
+ /// <returns>Task.</returns>
+ public async Task<TranscodingJobDto> StartFfMpeg(
+ StreamState state,
+ string outputPath,
+ string commandLineArguments,
+ HttpRequest request,
+ TranscodingJobType transcodingJobType,
+ CancellationTokenSource cancellationTokenSource,
+ string? workingDirectory = null)
+ {
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(directory);
+
+ await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
+
+ if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ var userId = request.HttpContext.User.GetUserId();
+ var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+ if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
- var userId = request.HttpContext.User.GetUserId();
- var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
- if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
- {
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+ this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
- throw new ArgumentException("User does not have access to video transcoding.");
- }
+ throw new ArgumentException("User does not have access to video transcoding.");
}
+ }
- ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
+ ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
- // If subtitles get burned in fonts may need to be extracted from the media file
- if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ // If subtitles get burned in fonts may need to be extracted from the media file
+ if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
+ if (state.VideoType != VideoType.Dvd)
{
- var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
-
- if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
- {
- string subtitlePath = state.SubtitleStream.Path;
- string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
- string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
- await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
- }
}
- var process = new Process
+ if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
{
- StartInfo = new ProcessStartInfo
- {
- WindowStyle = ProcessWindowStyle.Hidden,
- CreateNoWindow = true,
- UseShellExecute = false,
-
- // Must consume both stdout and stderr or deadlocks may occur
- // RedirectStandardOutput = true,
- RedirectStandardError = true,
- RedirectStandardInput = true,
- FileName = _mediaEncoder.EncoderPath,
- Arguments = commandLineArguments,
- WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- };
+ string subtitlePath = state.SubtitleStream.Path;
+ string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
+ string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var transcodingJob = this.OnTranscodeBeginning(
- outputPath,
- state.Request.PlaySessionId,
- state.MediaSource.LiveStreamId,
- Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- transcodingJobType,
- process,
- state.Request.DeviceId,
- state,
- cancellationTokenSource);
-
- _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
-
- var logFilePrefix = "FFmpeg.Transcode-";
- if (state.VideoRequest is not null
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
- ? "FFmpeg.Remux-"
- : "FFmpeg.DirectStream-";
+ await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
+ }
- var logFilePath = Path.Combine(
- _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
- $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ WindowStyle = ProcessWindowStyle.Hidden,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+
+ // Must consume both stdout and stderr or deadlocks may occur
+ // RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ RedirectStandardInput = true,
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = commandLineArguments,
+ WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ };
+
+ var transcodingJob = this.OnTranscodeBeginning(
+ outputPath,
+ state.Request.PlaySessionId,
+ state.MediaSource.LiveStreamId,
+ Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
+ transcodingJobType,
+ process,
+ state.Request.DeviceId,
+ state,
+ cancellationTokenSource);
+
+ _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ var logFilePrefix = "FFmpeg.Transcode-";
+ if (state.VideoRequest is not null
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
+ ? "FFmpeg.Remux-"
+ : "FFmpeg.DirectStream-";
+ }
- // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ var logFilePath = Path.Combine(
+ _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
+ $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
- var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
- var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
+ // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+ Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
+ var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+ var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
+ await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
- try
- {
- process.Start();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting FFmpeg");
+ process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+ try
+ {
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting FFmpeg");
- throw;
- }
+ this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
- _logger.LogDebug("Launched FFmpeg process");
- state.TranscodingJob = transcodingJob;
+ throw;
+ }
- // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
- _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
+ _logger.LogDebug("Launched FFmpeg process");
+ state.TranscodingJob = transcodingJob;
- // Wait for the file to exist before proceeding
- var ffmpegTargetFile = state.WaitForPath ?? outputPath;
- _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
- while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
- {
- await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
- }
+ // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
+ _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
- _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
+ // Wait for the file to exist before proceeding
+ var ffmpegTargetFile = state.WaitForPath ?? outputPath;
+ _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
+ while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
+ {
+ await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
- if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
- {
- await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
+ _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
- if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
- {
- await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
+ if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
+ {
+ await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
- if (!transcodingJob.HasExited)
+ if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
{
- StartThrottler(state, transcodingJob);
+ await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
}
- else if (transcodingJob.ExitCode != 0)
- {
- throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
- }
-
- _logger.LogDebug("StartFfMpeg() finished successfully");
+ }
- return transcodingJob;
+ if (!transcodingJob.HasExited)
+ {
+ StartThrottler(state, transcodingJob);
}
+ else if (transcodingJob.ExitCode != 0)
+ {
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
+ }
+
+ _logger.LogDebug("StartFfMpeg() finished successfully");
- private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
+ return transcodingJob;
+ }
+
+ private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
+ {
+ if (EnableThrottling(state))
{
- if (EnableThrottling(state))
- {
- transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
- transcodingJob.TranscodingThrottler.Start();
- }
+ transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
+ transcodingJob.TranscodingThrottler.Start();
}
+ }
- private bool EnableThrottling(StreamState state)
+ private bool EnableThrottling(StreamState state)
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+
+ return state.InputProtocol == MediaProtocol.File &&
+ state.RunTimeTicks.HasValue &&
+ state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
+ state.IsInputVideo &&
+ state.VideoType == VideoType.VideoFile;
+ }
+
+ /// <summary>
+ /// Called when [transcode beginning].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="playSessionId">The play session identifier.</param>
+ /// <param name="liveStreamId">The live stream identifier.</param>
+ /// <param name="transcodingJobId">The transcoding job identifier.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="process">The process.</param>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="state">The state.</param>
+ /// <param name="cancellationTokenSource">The cancellation token source.</param>
+ /// <returns>TranscodingJob.</returns>
+ public TranscodingJobDto OnTranscodeBeginning(
+ string path,
+ string? playSessionId,
+ string? liveStreamId,
+ string transcodingJobId,
+ TranscodingJobType type,
+ Process process,
+ string? deviceId,
+ StreamState state,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ lock (_activeTranscodingJobs)
{
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>())
+ {
+ Type = type,
+ Path = path,
+ Process = process,
+ ActiveRequestCount = 1,
+ DeviceId = deviceId,
+ CancellationTokenSource = cancellationTokenSource,
+ Id = transcodingJobId,
+ PlaySessionId = playSessionId,
+ LiveStreamId = liveStreamId,
+ MediaSource = state.MediaSource
+ };
- return state.InputProtocol == MediaProtocol.File &&
- state.RunTimeTicks.HasValue &&
- state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
- state.IsInputVideo &&
- state.VideoType == VideoType.VideoFile;
- }
-
- /// <summary>
- /// Called when [transcode beginning].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="playSessionId">The play session identifier.</param>
- /// <param name="liveStreamId">The live stream identifier.</param>
- /// <param name="transcodingJobId">The transcoding job identifier.</param>
- /// <param name="type">The type.</param>
- /// <param name="process">The process.</param>
- /// <param name="deviceId">The device id.</param>
- /// <param name="state">The state.</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <returns>TranscodingJob.</returns>
- public TranscodingJobDto OnTranscodeBeginning(
- string path,
- string? playSessionId,
- string? liveStreamId,
- string transcodingJobId,
- TranscodingJobType type,
- Process process,
- string? deviceId,
- StreamState state,
- CancellationTokenSource cancellationTokenSource)
- {
- lock (_activeTranscodingJobs)
- {
- var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>())
- {
- Type = type,
- Path = path,
- Process = process,
- ActiveRequestCount = 1,
- DeviceId = deviceId,
- CancellationTokenSource = cancellationTokenSource,
- Id = transcodingJobId,
- PlaySessionId = playSessionId,
- LiveStreamId = liveStreamId,
- MediaSource = state.MediaSource
- };
-
- _activeTranscodingJobs.Add(job);
-
- ReportTranscodingProgress(job, state, null, null, null, null, null);
-
- return job;
- }
+ _activeTranscodingJobs.Add(job);
+
+ ReportTranscodingProgress(job, state, null, null, null, null, null);
+
+ return job;
}
+ }
- /// <summary>
- /// Called when [transcode end].
- /// </summary>
- /// <param name="job">The transcode job.</param>
- public void OnTranscodeEndRequest(TranscodingJobDto job)
+ /// <summary>
+ /// Called when [transcode end].
+ /// </summary>
+ /// <param name="job">The transcode job.</param>
+ public void OnTranscodeEndRequest(TranscodingJobDto job)
+ {
+ job.ActiveRequestCount--;
+ _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
+ if (job.ActiveRequestCount <= 0)
{
- job.ActiveRequestCount--;
- _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
- if (job.ActiveRequestCount <= 0)
- {
- PingTimer(job, false);
- }
+ PingTimer(job, false);
}
+ }
- /// <summary>
- /// <summary>
- /// The progressive
- /// </summary>
- /// Called when [transcode failed to start].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="type">The type.</param>
- /// <param name="state">The state.</param>
- public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
+ /// <summary>
+ /// <summary>
+ /// The progressive
+ /// </summary>
+ /// Called when [transcode failed to start].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="state">The state.</param>
+ public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
+ {
+ lock (_activeTranscodingJobs)
{
- lock (_activeTranscodingJobs)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
- if (job is not null)
- {
- _activeTranscodingJobs.Remove(job);
- }
- }
+ var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
- lock (_transcodingLocks)
+ if (job is not null)
{
- _transcodingLocks.Remove(path);
+ _activeTranscodingJobs.Remove(job);
}
+ }
- if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
- {
- _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
- }
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(path);
}
- /// <summary>
- /// Processes the exited.
- /// </summary>
- /// <param name="process">The process.</param>
- /// <param name="job">The job.</param>
- /// <param name="state">The state.</param>
- private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
+ if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{
- job.HasExited = true;
- job.ExitCode = process.ExitCode;
+ _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
+ }
+ }
- ReportTranscodingProgress(job, state, null, null, null, null, null);
+ /// <summary>
+ /// Processes the exited.
+ /// </summary>
+ /// <param name="process">The process.</param>
+ /// <param name="job">The job.</param>
+ /// <param name="state">The state.</param>
+ private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
+ {
+ job.HasExited = true;
+ job.ExitCode = process.ExitCode;
- _logger.LogDebug("Disposing stream resources");
- state.Dispose();
+ ReportTranscodingProgress(job, state, null, null, null, null, null);
- if (process.ExitCode == 0)
- {
- _logger.LogInformation("FFmpeg exited with code 0");
- }
- else
- {
- _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
- }
+ _logger.LogDebug("Disposing stream resources");
+ state.Dispose();
- job.Dispose();
+ if (process.ExitCode == 0)
+ {
+ _logger.LogInformation("FFmpeg exited with code 0");
}
-
- private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
+ else
{
- if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
- {
- var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
- new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
+ }
- _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
+ job.Dispose();
+ }
- if (state.VideoRequest is not null)
- {
- _encodingHelper.TryStreamCopy(state);
- }
- }
+ private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
+ {
+ if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
+ {
+ var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
+ new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+
+ _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
- if (state.MediaSource.BufferMs.HasValue)
+ if (state.VideoRequest is not null)
{
- await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
+ _encodingHelper.TryStreamCopy(state);
}
}
- /// <summary>
- /// Called when [transcode begin request].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="type">The type.</param>
- /// <returns>The <see cref="TranscodingJobDto"/>.</returns>
- public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type)
+ if (state.MediaSource.BufferMs.HasValue)
{
- lock (_activeTranscodingJobs)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
- if (job is null)
- {
- return null;
- }
-
- OnTranscodeBeginRequest(job);
-
- return job;
- }
+ await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
}
+ }
- private void OnTranscodeBeginRequest(TranscodingJobDto job)
+ /// <summary>
+ /// Called when [transcode begin request].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>The <see cref="TranscodingJobDto"/>.</returns>
+ public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type)
+ {
+ lock (_activeTranscodingJobs)
{
- job.ActiveRequestCount++;
+ var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
- if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
+ if (job is null)
{
- job.StopKillTimer();
+ return null;
}
+
+ OnTranscodeBeginRequest(job);
+
+ return job;
}
+ }
- /// <summary>
- /// Gets the transcoding lock.
- /// </summary>
- /// <param name="outputPath">The output path of the transcoded file.</param>
- /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
- public SemaphoreSlim GetTranscodingLock(string outputPath)
- {
- lock (_transcodingLocks)
- {
- if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
- {
- result = new SemaphoreSlim(1, 1);
- _transcodingLocks[outputPath] = result;
- }
+ private void OnTranscodeBeginRequest(TranscodingJobDto job)
+ {
+ job.ActiveRequestCount++;
- return result;
- }
+ if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
+ {
+ job.StopKillTimer();
}
+ }
- private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
+ /// <summary>
+ /// Gets the transcoding lock.
+ /// </summary>
+ /// <param name="outputPath">The output path of the transcoded file.</param>
+ /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
+ public SemaphoreSlim GetTranscodingLock(string outputPath)
+ {
+ lock (_transcodingLocks)
{
- if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
+ if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
{
- PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+ result = new SemaphoreSlim(1, 1);
+ _transcodingLocks[outputPath] = result;
}
+
+ return result;
}
+ }
- /// <summary>
- /// Deletes the encoded media cache.
- /// </summary>
- private void DeleteEncodedMediaCache()
+ private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
+ {
+ if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{
- var path = _serverConfigurationManager.GetTranscodePath();
- if (!Directory.Exists(path))
- {
- return;
- }
+ PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+ }
+ }
- foreach (var file in _fileSystem.GetFilePaths(path, true))
- {
- _fileSystem.DeleteFile(file);
- }
+ /// <summary>
+ /// Deletes the encoded media cache.
+ /// </summary>
+ private void DeleteEncodedMediaCache()
+ {
+ var path = _serverConfigurationManager.GetTranscodePath();
+ if (!Directory.Exists(path))
+ {
+ return;
}
- /// <summary>
- /// Dispose transcoding job helper.
- /// </summary>
- public void Dispose()
+ foreach (var file in _fileSystem.GetFilePaths(path, true))
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ _fileSystem.DeleteFile(file);
}
+ }
- /// <summary>
- /// Dispose throttler.
- /// </summary>
- /// <param name="disposing">Disposing.</param>
- protected virtual void Dispose(bool disposing)
+ /// <summary>
+ /// Dispose transcoding job helper.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose throttler.
+ /// </summary>
+ /// <param name="disposing">Disposing.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
{
- if (disposing)
- {
- _loggerFactory.Dispose();
- _sessionManager.PlaybackProgress -= OnPlaybackProgress;
- _sessionManager.PlaybackStart -= OnPlaybackProgress;
- }
+ _loggerFactory.Dispose();
+ _sessionManager.PlaybackProgress -= OnPlaybackProgress;
+ _sessionManager.PlaybackStart -= OnPlaybackProgress;
}
}
}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index b5444138f..6a0a4706b 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -13,27 +13,28 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.2" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
- <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" />
+ <PackageReference Include="Microsoft.Extensions.Http" />
+ <PackageReference Include="Swashbuckle.AspNetCore" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
new file mode 100644
index 000000000..7bcc328aa
--- /dev/null
+++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Redirect requests without baseurl prefix to the baseurl prefixed URL.
+/// </summary>
+public class BaseUrlRedirectionMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger<BaseUrlRedirectionMiddleware> _logger;
+ private readonly IConfiguration _configuration;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="configuration">The application configuration.</param>
+ public BaseUrlRedirectionMiddleware(
+ RequestDelegate next,
+ ILogger<BaseUrlRedirectionMiddleware> logger,
+ IConfiguration configuration)
+ {
+ _next = next;
+ _logger = logger;
+ _configuration = configuration;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
+ {
+ var localPath = httpContext.Request.Path.ToString();
+ var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
+
+ if (string.IsNullOrEmpty(localPath)
+ || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase)
+ || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
+ )
+ {
+ // Redirect health endpoint
+ if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(localPath, "/health/", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Redirecting /health check");
+ httpContext.Response.Redirect(baseUrlPrefix + "/health");
+ return;
+ }
+
+ // Always redirect back to the default path if the base prefix is invalid or missing
+ _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
+
+ var port = httpContext.Request.Host.Port ?? -1;
+ var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri;
+ var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri;
+ var target = uri.MakeRelativeUri(redirectUri).ToString();
+ _logger.LogDebug("Redirecting to {Target}", target);
+
+ httpContext.Response.Redirect(target);
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
new file mode 100644
index 000000000..060c14f89
--- /dev/null
+++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
@@ -0,0 +1,150 @@
+using System;
+using System.IO;
+using System.Net.Mime;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Exception Middleware.
+/// </summary>
+public class ExceptionMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger<ExceptionMiddleware> _logger;
+ private readonly IServerConfigurationManager _configuration;
+ private readonly IWebHostEnvironment _hostEnvironment;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">Next request delegate.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param>
+ public ExceptionMiddleware(
+ RequestDelegate next,
+ ILogger<ExceptionMiddleware> logger,
+ IServerConfigurationManager serverConfigurationManager,
+ IWebHostEnvironment hostEnvironment)
+ {
+ _next = next;
+ _logger = logger;
+ _configuration = serverConfigurationManager;
+ _hostEnvironment = hostEnvironment;
+ }
+
+ /// <summary>
+ /// Invoke request.
+ /// </summary>
+ /// <param name="context">Request context.</param>
+ /// <returns>Task.</returns>
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ await _next(context).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ if (context.Response.HasStarted)
+ {
+ _logger.LogWarning("The response has already started, the exception middleware will not be executed.");
+ throw;
+ }
+
+ ex = GetActualException(ex);
+
+ bool ignoreStackTrace =
+ ex is SocketException
+ || ex is IOException
+ || ex is OperationCanceledException
+ || ex is SecurityException
+ || ex is AuthenticationException
+ || ex is FileNotFoundException;
+
+ if (ignoreStackTrace)
+ {
+ _logger.LogError(
+ "Error processing request: {ExceptionMessage}. URL {Method} {Url}.",
+ ex.Message.TrimEnd('.'),
+ context.Request.Method,
+ context.Request.Path);
+ }
+ else
+ {
+ _logger.LogError(
+ ex,
+ "Error processing request. URL {Method} {Url}.",
+ context.Request.Method,
+ context.Request.Path);
+ }
+
+ context.Response.StatusCode = GetStatusCode(ex);
+ context.Response.ContentType = MediaTypeNames.Text.Plain;
+
+ // Don't send exception unless the server is in a Development environment
+ var errorContent = _hostEnvironment.IsDevelopment()
+ ? NormalizeExceptionMessage(ex.Message)
+ : "Error processing request.";
+ await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
+ }
+ }
+
+ private static Exception GetActualException(Exception ex)
+ {
+ if (ex is AggregateException agg)
+ {
+ var inner = agg.InnerException;
+ if (inner is not null)
+ {
+ return GetActualException(inner);
+ }
+
+ var inners = agg.InnerExceptions;
+ if (inners.Count > 0)
+ {
+ return GetActualException(inners[0]);
+ }
+ }
+
+ return ex;
+ }
+
+ private static int GetStatusCode(Exception ex)
+ {
+ switch (ex)
+ {
+ case ArgumentException _: return StatusCodes.Status400BadRequest;
+ case AuthenticationException _: return StatusCodes.Status401Unauthorized;
+ case SecurityException _: return StatusCodes.Status403Forbidden;
+ case DirectoryNotFoundException _:
+ case FileNotFoundException _:
+ case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
+ case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
+ default: return StatusCodes.Status500InternalServerError;
+ }
+ }
+
+ private string NormalizeExceptionMessage(string msg)
+ {
+ // Strip any information we don't want to reveal
+ return msg.Replace(
+ _configuration.ApplicationPaths.ProgramSystemPath,
+ string.Empty,
+ StringComparison.OrdinalIgnoreCase)
+ .Replace(
+ _configuration.ApplicationPaths.ProgramDataPath,
+ string.Empty,
+ StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs
new file mode 100644
index 000000000..f45b6b5c0
--- /dev/null
+++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -0,0 +1,49 @@
+using System.Net;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Validates the IP of requests coming from local networks wrt. remote access.
+/// </summary>
+public class IpBasedAccessValidationMiddleware
+{
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public IpBasedAccessValidationMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="networkManager">The network manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, INetworkManager networkManager)
+ {
+ if (httpContext.IsLocal())
+ {
+ // Running locally.
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
+
+ if (!networkManager.HasRemoteAccess(remoteIp))
+ {
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
new file mode 100644
index 000000000..9c2194faf
--- /dev/null
+++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
@@ -0,0 +1,49 @@
+using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Validates the LAN host IP based on application configuration.
+/// </summary>
+public class LanFilteringMiddleware
+{
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public LanFilteringMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="networkManager">The network manager.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
+ {
+ if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ var host = httpContext.GetNormalizedRemoteIp();
+ if (!networkManager.IsInLocalNetwork(host))
+ {
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
new file mode 100644
index 000000000..17d8997d5
--- /dev/null
+++ b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Removes /emby and /mediabrowser from requested route.
+/// </summary>
+public class LegacyEmbyRouteRewriteMiddleware
+{
+ private const string EmbyPath = "/emby";
+ private const string MediabrowserPath = "/mediabrowser";
+
+ private readonly RequestDelegate _next;
+ private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ /// <param name="logger">The logger.</param>
+ public LegacyEmbyRouteRewriteMiddleware(
+ RequestDelegate next,
+ ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var localPath = httpContext.Request.Path.ToString();
+ if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
+ {
+ httpContext.Request.Path = localPath[EmbyPath.Length..];
+ _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
+ }
+ else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
+ {
+ httpContext.Request.Path = localPath[MediabrowserPath.Length..];
+ _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs
new file mode 100644
index 000000000..cb4169e99
--- /dev/null
+++ b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs
@@ -0,0 +1,38 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// URL decodes the querystring before binding.
+/// </summary>
+public class QueryStringDecodingMiddleware
+{
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public QueryStringDecodingMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var feature = httpContext.Features.Get<IQueryFeature>();
+ if (feature is not null)
+ {
+ httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature));
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs
new file mode 100644
index 000000000..db3917743
--- /dev/null
+++ b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs
@@ -0,0 +1,68 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Response time middleware.
+/// </summary>
+public class ResponseTimeMiddleware
+{
+ private const string ResponseHeaderResponseTime = "X-Response-Time-ms";
+
+ private readonly RequestDelegate _next;
+ private readonly ILogger<ResponseTimeMiddleware> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">Next request delegate.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
+ public ResponseTimeMiddleware(
+ RequestDelegate next,
+ ILogger<ResponseTimeMiddleware> logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Invoke request.
+ /// </summary>
+ /// <param name="context">Request context.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <returns>Task.</returns>
+ public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
+ {
+ var startTimestamp = Stopwatch.GetTimestamp();
+
+ var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
+ var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
+ context.Response.OnStarting(() =>
+ {
+ var responseTime = Stopwatch.GetElapsedTime(startTimestamp);
+ var responseTimeMs = responseTime.TotalMilliseconds;
+ if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.LogDebug(
+ "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
+ context.Request.GetDisplayUrl(),
+ context.GetNormalizedRemoteIp(),
+ responseTime,
+ context.Response.StatusCode);
+ }
+
+ context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture);
+ return Task.CompletedTask;
+ });
+
+ // Call the next delegate/middleware in the pipeline
+ await this._next(context).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
new file mode 100644
index 000000000..8bf626035
--- /dev/null
+++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Redirect requests to robots.txt to web/robots.txt.
+/// </summary>
+public class RobotsRedirectionMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger<RobotsRedirectionMiddleware> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ /// <param name="logger">The logger.</param>
+ public RobotsRedirectionMiddleware(
+ RequestDelegate next,
+ ILogger<RobotsRedirectionMiddleware> logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var localPath = httpContext.Request.Path.ToString();
+ if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
+ httpContext.Response.Redirect("web/robots.txt");
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs
new file mode 100644
index 000000000..dcb234658
--- /dev/null
+++ b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Shows a custom message during server startup.
+/// </summary>
+public class ServerStartupMessageMiddleware
+{
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public ServerStartupMessageMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="serverApplicationHost">The server application host.</param>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(
+ HttpContext httpContext,
+ IServerApplicationHost serverApplicationHost,
+ ILocalizationManager localizationManager)
+ {
+ if (serverApplicationHost.CoreStartupHasCompleted
+ || httpContext.Request.Path.Equals("/system/ping", StringComparison.OrdinalIgnoreCase))
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
+ httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
+ httpContext.Response.ContentType = MediaTypeNames.Text.Html;
+ await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs
new file mode 100644
index 000000000..f75d0d24e
--- /dev/null
+++ b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Extensions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Defines the <see cref="UrlDecodeQueryFeature"/>.
+/// </summary>
+public class UrlDecodeQueryFeature : IQueryFeature
+{
+ private IQueryCollection? _store;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class.
+ /// </summary>
+ /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param>
+ public UrlDecodeQueryFeature(IQueryFeature feature)
+ {
+ Query = feature.Query;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>.
+ /// </summary>
+ public IQueryCollection Query
+ {
+ get
+ {
+ return _store ?? QueryCollection.Empty;
+ }
+
+ set
+ {
+ // Only interested in where the querystring is encoded which shows up as one key with nothing in the value.
+ if (value.Count != 1)
+ {
+ _store = value;
+ return;
+ }
+
+ // Encoded querystrings have no value, so don't process anything if a value is present.
+ var (key, stringValues) = value.First();
+ if (!string.IsNullOrEmpty(stringValues))
+ {
+ _store = value;
+ return;
+ }
+
+ if (!key.Contains('=', StringComparison.Ordinal))
+ {
+ _store = value;
+ return;
+ }
+
+ var pairs = new Dictionary<string, StringValues>();
+ foreach (var pair in key.SpanSplit('&'))
+ {
+ var i = pair.IndexOf('=');
+ if (i == -1)
+ {
+ // encoded is an equals.
+ // We use TryAdd so duplicate keys get ignored
+ pairs.TryAdd(pair.ToString(), StringValues.Empty);
+ continue;
+ }
+
+ var k = pair[..i].ToString();
+ var v = pair[(i + 1)..].ToString();
+ if (!pairs.TryAdd(k, new StringValues(v)))
+ {
+ pairs[k] = StringValues.Concat(pairs[k], v);
+ }
+ }
+
+ _store = new QueryCollection(pairs);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs
new file mode 100644
index 000000000..009fb6269
--- /dev/null
+++ b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs
@@ -0,0 +1,39 @@
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Middleware;
+
+/// <summary>
+/// Handles WebSocket requests.
+/// </summary>
+public class WebSocketHandlerMiddleware
+{
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public WebSocketHandlerMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="webSocketManager">The WebSocket connection manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager)
+ {
+ if (!httpContext.WebSockets.IsWebSocketRequest)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
index 75e47a71b..a34fd01d5 100644
--- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
@@ -5,86 +5,85 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.ModelBinders
+namespace Jellyfin.Api.ModelBinders;
+
+/// <summary>
+/// Comma delimited array model binder.
+/// Returns an empty array of specified type if there is no query parameter.
+/// </summary>
+public class CommaDelimitedArrayModelBinder : IModelBinder
{
+ private readonly ILogger<CommaDelimitedArrayModelBinder> _logger;
+
/// <summary>
- /// Comma delimited array model binder.
- /// Returns an empty array of specified type if there is no query parameter.
+ /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
/// </summary>
- public class CommaDelimitedArrayModelBinder : IModelBinder
+ /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param>
+ public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger)
+ {
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public Task BindModelAsync(ModelBindingContext bindingContext)
{
- private readonly ILogger<CommaDelimitedArrayModelBinder> _logger;
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
+ var converter = TypeDescriptor.GetConverter(elementType);
- /// <summary>
- /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param>
- public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger)
+ if (valueProviderResult.Length > 1)
{
- _logger = logger;
+ var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
+ bindingContext.Result = ModelBindingResult.Success(typedValues);
}
-
- /// <inheritdoc/>
- public Task BindModelAsync(ModelBindingContext bindingContext)
+ else
{
- var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
- var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
- var converter = TypeDescriptor.GetConverter(elementType);
+ var value = valueProviderResult.FirstValue;
- if (valueProviderResult.Length > 1)
+ if (value is not null)
{
- var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
+ var splitValues = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ var typedValues = GetParsedResult(splitValues, elementType, converter);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
- var value = valueProviderResult.FirstValue;
-
- if (value is not null)
- {
- var splitValues = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
- var typedValues = GetParsedResult(splitValues, elementType, converter);
- bindingContext.Result = ModelBindingResult.Success(typedValues);
- }
- else
- {
- var emptyResult = Array.CreateInstance(elementType, 0);
- bindingContext.Result = ModelBindingResult.Success(emptyResult);
- }
+ var emptyResult = Array.CreateInstance(elementType, 0);
+ bindingContext.Result = ModelBindingResult.Success(emptyResult);
}
-
- return Task.CompletedTask;
}
- private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
+ return Task.CompletedTask;
+ }
+
+ private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
+ {
+ var parsedValues = new object?[values.Count];
+ var convertedCount = 0;
+ for (var i = 0; i < values.Count; i++)
{
- var parsedValues = new object?[values.Count];
- var convertedCount = 0;
- for (var i = 0; i < values.Count; i++)
+ try
{
- try
- {
- parsedValues[i] = converter.ConvertFromString(values[i].Trim());
- convertedCount++;
- }
- catch (FormatException e)
- {
- _logger.LogDebug(e, "Error converting value.");
- }
+ parsedValues[i] = converter.ConvertFromString(values[i].Trim());
+ convertedCount++;
}
-
- var typedValues = Array.CreateInstance(elementType, convertedCount);
- var typedValueIndex = 0;
- for (var i = 0; i < parsedValues.Length; i++)
+ catch (FormatException e)
{
- if (parsedValues[i] != null)
- {
- typedValues.SetValue(parsedValues[i], typedValueIndex);
- typedValueIndex++;
- }
+ _logger.LogDebug(e, "Error converting value.");
}
+ }
- return typedValues;
+ var typedValues = Array.CreateInstance(elementType, convertedCount);
+ var typedValueIndex = 0;
+ for (var i = 0; i < parsedValues.Length; i++)
+ {
+ if (parsedValues[i] != null)
+ {
+ typedValues.SetValue(parsedValues[i], typedValueIndex);
+ typedValueIndex++;
+ }
}
+
+ return typedValues;
}
}
diff --git a/Jellyfin.Api/ModelBinders/LegacyDateTimeModelBinder.cs b/Jellyfin.Api/ModelBinders/LegacyDateTimeModelBinder.cs
index e1cb725f3..87a30773e 100644
--- a/Jellyfin.Api/ModelBinders/LegacyDateTimeModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/LegacyDateTimeModelBinder.cs
@@ -5,45 +5,44 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.ModelBinders
+namespace Jellyfin.Api.ModelBinders;
+
+/// <summary>
+/// DateTime model binder.
+/// </summary>
+public class LegacyDateTimeModelBinder : IModelBinder
{
+ // Borrowed from the DateTimeModelBinderProvider
+ private const DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces;
+ private readonly DateTimeModelBinder _defaultModelBinder;
+
/// <summary>
- /// DateTime model binder.
+ /// Initializes a new instance of the <see cref="LegacyDateTimeModelBinder"/> class.
/// </summary>
- public class LegacyDateTimeModelBinder : IModelBinder
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public LegacyDateTimeModelBinder(ILoggerFactory loggerFactory)
{
- // Borrowed from the DateTimeModelBinderProvider
- private const DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces;
- private readonly DateTimeModelBinder _defaultModelBinder;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LegacyDateTimeModelBinder"/> class.
- /// </summary>
- /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
- public LegacyDateTimeModelBinder(ILoggerFactory loggerFactory)
- {
- _defaultModelBinder = new DateTimeModelBinder(SupportedStyles, loggerFactory);
- }
+ _defaultModelBinder = new DateTimeModelBinder(SupportedStyles, loggerFactory);
+ }
- /// <inheritdoc />
- public Task BindModelAsync(ModelBindingContext bindingContext)
+ /// <inheritdoc />
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult.Values.Count == 1)
{
- var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
- if (valueProviderResult.Values.Count == 1)
+ var dateTimeString = valueProviderResult.FirstValue;
+ // Mark Played Item.
+ if (DateTime.TryParseExact(dateTimeString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime))
{
- var dateTimeString = valueProviderResult.FirstValue;
- // Mark Played Item.
- if (DateTime.TryParseExact(dateTimeString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime))
- {
- bindingContext.Result = ModelBindingResult.Success(dateTime);
- }
- else
- {
- return _defaultModelBinder.BindModelAsync(bindingContext);
- }
+ bindingContext.Result = ModelBindingResult.Success(dateTime);
+ }
+ else
+ {
+ return _defaultModelBinder.BindModelAsync(bindingContext);
}
-
- return Task.CompletedTask;
}
+
+ return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
index d2e78ac88..a2e139ca1 100644
--- a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
@@ -4,45 +4,44 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.ModelBinders
+namespace Jellyfin.Api.ModelBinders;
+
+/// <summary>
+/// Nullable enum model binder.
+/// </summary>
+public class NullableEnumModelBinder : IModelBinder
{
+ private readonly ILogger<NullableEnumModelBinder> _logger;
+
/// <summary>
- /// Nullable enum model binder.
+ /// Initializes a new instance of the <see cref="NullableEnumModelBinder"/> class.
/// </summary>
- public class NullableEnumModelBinder : IModelBinder
+ /// <param name="logger">Instance of the <see cref="ILogger{NullableEnumModelBinder}"/> interface.</param>
+ public NullableEnumModelBinder(ILogger<NullableEnumModelBinder> logger)
{
- private readonly ILogger<NullableEnumModelBinder> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="NullableEnumModelBinder"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{NullableEnumModelBinder}"/> interface.</param>
- public NullableEnumModelBinder(ILogger<NullableEnumModelBinder> logger)
- {
- _logger = logger;
- }
+ _logger = logger;
+ }
- /// <inheritdoc />
- public Task BindModelAsync(ModelBindingContext bindingContext)
+ /// <inheritdoc />
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
+ var converter = TypeDescriptor.GetConverter(elementType);
+ if (valueProviderResult.Length != 0)
{
- var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
- var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
- var converter = TypeDescriptor.GetConverter(elementType);
- if (valueProviderResult.Length != 0)
+ try
{
- try
- {
- // REVIEW: This shouldn't be null here
- var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue!);
- bindingContext.Result = ModelBindingResult.Success(convertedValue);
- }
- catch (FormatException e)
- {
- _logger.LogDebug(e, "Error converting value.");
- }
+ // REVIEW: This shouldn't be null here
+ var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue!);
+ bindingContext.Result = ModelBindingResult.Success(convertedValue);
+ }
+ catch (FormatException e)
+ {
+ _logger.LogDebug(e, "Error converting value.");
}
-
- return Task.CompletedTask;
}
+
+ return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs b/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs
index da0addd0e..43ffdaefd 100644
--- a/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs
+++ b/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs
@@ -3,25 +3,24 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.ModelBinders
+namespace Jellyfin.Api.ModelBinders;
+
+/// <summary>
+/// Nullable enum model binder provider.
+/// </summary>
+public class NullableEnumModelBinderProvider : IModelBinderProvider
{
- /// <summary>
- /// Nullable enum model binder provider.
- /// </summary>
- public class NullableEnumModelBinderProvider : IModelBinderProvider
+ /// <inheritdoc />
+ public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
- /// <inheritdoc />
- public IModelBinder? GetBinder(ModelBinderProviderContext context)
+ var nullableType = Nullable.GetUnderlyingType(context.Metadata.ModelType);
+ if (nullableType is null || !nullableType.IsEnum)
{
- var nullableType = Nullable.GetUnderlyingType(context.Metadata.ModelType);
- if (nullableType is null || !nullableType.IsEnum)
- {
- // Type isn't nullable or isn't an enum.
- return null;
- }
-
- var logger = context.Services.GetRequiredService<ILogger<NullableEnumModelBinder>>();
- return new NullableEnumModelBinder(logger);
+ // Type isn't nullable or isn't an enum.
+ return null;
}
+
+ var logger = context.Services.GetRequiredService<ILogger<NullableEnumModelBinder>>();
+ return new NullableEnumModelBinder(logger);
}
}
diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
index 4257ba0e2..cb9a82955 100644
--- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
@@ -5,86 +5,85 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.ModelBinders
+namespace Jellyfin.Api.ModelBinders;
+
+/// <summary>
+/// Comma delimited array model binder.
+/// Returns an empty array of specified type if there is no query parameter.
+/// </summary>
+public class PipeDelimitedArrayModelBinder : IModelBinder
{
+ private readonly ILogger<PipeDelimitedArrayModelBinder> _logger;
+
/// <summary>
- /// Comma delimited array model binder.
- /// Returns an empty array of specified type if there is no query parameter.
+ /// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class.
/// </summary>
- public class PipeDelimitedArrayModelBinder : IModelBinder
+ /// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param>
+ public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> logger)
+ {
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public Task BindModelAsync(ModelBindingContext bindingContext)
{
- private readonly ILogger<PipeDelimitedArrayModelBinder> _logger;
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
+ var converter = TypeDescriptor.GetConverter(elementType);
- /// <summary>
- /// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param>
- public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> logger)
+ if (valueProviderResult.Length > 1)
{
- _logger = logger;
+ var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
+ bindingContext.Result = ModelBindingResult.Success(typedValues);
}
-
- /// <inheritdoc/>
- public Task BindModelAsync(ModelBindingContext bindingContext)
+ else
{
- var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
- var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
- var converter = TypeDescriptor.GetConverter(elementType);
+ var value = valueProviderResult.FirstValue;
- if (valueProviderResult.Length > 1)
+ if (value is not null)
{
- var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
+ var splitValues = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
+ var typedValues = GetParsedResult(splitValues, elementType, converter);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
- var value = valueProviderResult.FirstValue;
-
- if (value is not null)
- {
- var splitValues = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
- var typedValues = GetParsedResult(splitValues, elementType, converter);
- bindingContext.Result = ModelBindingResult.Success(typedValues);
- }
- else
- {
- var emptyResult = Array.CreateInstance(elementType, 0);
- bindingContext.Result = ModelBindingResult.Success(emptyResult);
- }
+ var emptyResult = Array.CreateInstance(elementType, 0);
+ bindingContext.Result = ModelBindingResult.Success(emptyResult);
}
-
- return Task.CompletedTask;
}
- private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
+ return Task.CompletedTask;
+ }
+
+ private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
+ {
+ var parsedValues = new object?[values.Count];
+ var convertedCount = 0;
+ for (var i = 0; i < values.Count; i++)
{
- var parsedValues = new object?[values.Count];
- var convertedCount = 0;
- for (var i = 0; i < values.Count; i++)
+ try
{
- try
- {
- parsedValues[i] = converter.ConvertFromString(values[i].Trim());
- convertedCount++;
- }
- catch (FormatException e)
- {
- _logger.LogDebug(e, "Error converting value.");
- }
+ parsedValues[i] = converter.ConvertFromString(values[i].Trim());
+ convertedCount++;
}
-
- var typedValues = Array.CreateInstance(elementType, convertedCount);
- var typedValueIndex = 0;
- for (var i = 0; i < parsedValues.Length; i++)
+ catch (FormatException e)
{
- if (parsedValues[i] != null)
- {
- typedValues.SetValue(parsedValues[i], typedValueIndex);
- typedValueIndex++;
- }
+ _logger.LogDebug(e, "Error converting value.");
}
+ }
- return typedValues;
+ var typedValues = Array.CreateInstance(elementType, convertedCount);
+ var typedValueIndex = 0;
+ for (var i = 0; i < parsedValues.Length; i++)
+ {
+ if (parsedValues[i] != null)
+ {
+ typedValues.SetValue(parsedValues[i], typedValueIndex);
+ typedValueIndex++;
+ }
}
+
+ return typedValues;
}
}
diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs
index 44509a9c0..168247fd5 100644
--- a/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs
+++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs
@@ -1,22 +1,21 @@
-namespace Jellyfin.Api.Models.ClientLogDtos
+namespace Jellyfin.Api.Models.ClientLogDtos;
+
+/// <summary>
+/// Client log document response dto.
+/// </summary>
+public class ClientLogDocumentResponseDto
{
/// <summary>
- /// Client log document response dto.
+ /// Initializes a new instance of the <see cref="ClientLogDocumentResponseDto"/> class.
/// </summary>
- public class ClientLogDocumentResponseDto
+ /// <param name="fileName">The file name.</param>
+ public ClientLogDocumentResponseDto(string fileName)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="ClientLogDocumentResponseDto"/> class.
- /// </summary>
- /// <param name="fileName">The file name.</param>
- public ClientLogDocumentResponseDto(string fileName)
- {
- FileName = fileName;
- }
-
- /// <summary>
- /// Gets the resulting filename.
- /// </summary>
- public string FileName { get; }
+ FileName = fileName;
}
+
+ /// <summary>
+ /// Gets the resulting filename.
+ /// </summary>
+ public string FileName { get; }
}
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
index 3b827ec12..5a48345eb 100644
--- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
+++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Models.ConfigurationDtos
+namespace Jellyfin.Api.Models.ConfigurationDtos;
+
+/// <summary>
+/// Media Encoder Path Dto.
+/// </summary>
+public class MediaEncoderPathDto
{
/// <summary>
- /// Media Encoder Path Dto.
+ /// Gets or sets media encoder path.
/// </summary>
- public class MediaEncoderPathDto
- {
- /// <summary>
- /// Gets or sets media encoder path.
- /// </summary>
- public string Path { get; set; } = null!;
+ public string Path { get; set; } = null!;
- /// <summary>
- /// Gets or sets media encoder path type.
- /// </summary>
- public string PathType { get; set; } = null!;
- }
+ /// <summary>
+ /// Gets or sets media encoder path type.
+ /// </summary>
+ public string PathType { get; set; } = null!;
}
diff --git a/Jellyfin.Api/Models/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
index ec4a0d1a1..e7bcd6c53 100644
--- a/Jellyfin.Api/Models/ConfigurationPageInfo.cs
+++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
@@ -2,66 +2,65 @@ using System;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
-namespace Jellyfin.Api.Models
+namespace Jellyfin.Api.Models;
+
+/// <summary>
+/// The configuration page info.
+/// </summary>
+public class ConfigurationPageInfo
{
/// <summary>
- /// The configuration page info.
+ /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
/// </summary>
- public class ConfigurationPageInfo
+ /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
+ /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
+ public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
- /// </summary>
- /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
- /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
- public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page)
- {
- Name = page.Name;
- EnableInMainMenu = page.EnableInMainMenu;
- MenuSection = page.MenuSection;
- MenuIcon = page.MenuIcon;
- DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name : page.DisplayName;
- PluginId = plugin?.Id;
- }
+ Name = page.Name;
+ EnableInMainMenu = page.EnableInMainMenu;
+ MenuSection = page.MenuSection;
+ MenuIcon = page.MenuIcon;
+ DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name : page.DisplayName;
+ PluginId = plugin?.Id;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
- /// </summary>
- public ConfigurationPageInfo()
- {
- Name = string.Empty;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
+ /// </summary>
+ public ConfigurationPageInfo()
+ {
+ Name = string.Empty;
+ }
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether the configurations page is enabled in the main menu.
- /// </summary>
- public bool EnableInMainMenu { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the configurations page is enabled in the main menu.
+ /// </summary>
+ public bool EnableInMainMenu { get; set; }
- /// <summary>
- /// Gets or sets the menu section.
- /// </summary>
- public string? MenuSection { get; set; }
+ /// <summary>
+ /// Gets or sets the menu section.
+ /// </summary>
+ public string? MenuSection { get; set; }
- /// <summary>
- /// Gets or sets the menu icon.
- /// </summary>
- public string? MenuIcon { get; set; }
+ /// <summary>
+ /// Gets or sets the menu icon.
+ /// </summary>
+ public string? MenuIcon { get; set; }
- /// <summary>
- /// Gets or sets the display name.
- /// </summary>
- public string? DisplayName { get; set; }
+ /// <summary>
+ /// Gets or sets the display name.
+ /// </summary>
+ public string? DisplayName { get; set; }
- /// <summary>
- /// Gets or sets the plugin id.
- /// </summary>
- /// <value>The plugin id.</value>
- public Guid? PluginId { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the plugin id.
+ /// </summary>
+ /// <value>The plugin id.</value>
+ public Guid? PluginId { get; set; }
}
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
index 92be15b8a..c438e5a97 100644
--- a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
+++ b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
@@ -1,13 +1,12 @@
-namespace Jellyfin.Api.Models.EnvironmentDtos
+namespace Jellyfin.Api.Models.EnvironmentDtos;
+
+/// <summary>
+/// Default directory browser info.
+/// </summary>
+public class DefaultDirectoryBrowserInfoDto
{
/// <summary>
- /// Default directory browser info.
+ /// Gets or sets the path.
/// </summary>
- public class DefaultDirectoryBrowserInfoDto
- {
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- public string? Path { get; set; }
- }
+ public string? Path { get; set; }
}
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
index 418c11c2d..c54205bfa 100644
--- a/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
+++ b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
@@ -1,23 +1,22 @@
-namespace Jellyfin.Api.Models.EnvironmentDtos
+namespace Jellyfin.Api.Models.EnvironmentDtos;
+
+/// <summary>
+/// Validate path object.
+/// </summary>
+public class ValidatePathDto
{
/// <summary>
- /// Validate path object.
+ /// Gets or sets a value indicating whether validate if path is writable.
/// </summary>
- public class ValidatePathDto
- {
- /// <summary>
- /// Gets or sets a value indicating whether validate if path is writable.
- /// </summary>
- public bool ValidateWritable { get; set; }
+ public bool ValidateWritable { get; set; }
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- public string? Path { get; set; }
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ public string? Path { get; set; }
- /// <summary>
- /// Gets or sets is path file.
- /// </summary>
- public bool? IsFile { get; set; }
- }
+ /// <summary>
+ /// Gets or sets is path file.
+ /// </summary>
+ public bool? IsFile { get; set; }
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
index 358434434..6401522f6 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Models.LibraryDtos
+namespace Jellyfin.Api.Models.LibraryDtos;
+
+/// <summary>
+/// Library option info dto.
+/// </summary>
+public class LibraryOptionInfoDto
{
/// <summary>
- /// Library option info dto.
+ /// Gets or sets name.
/// </summary>
- public class LibraryOptionInfoDto
- {
- /// <summary>
- /// Gets or sets name.
- /// </summary>
- public string? Name { get; set; }
+ public string? Name { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether default enabled.
- /// </summary>
- public bool DefaultEnabled { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether default enabled.
+ /// </summary>
+ public bool DefaultEnabled { get; set; }
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
index 7de44aa65..78efacd94 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
@@ -1,31 +1,30 @@
using System;
using System.Collections.Generic;
-namespace Jellyfin.Api.Models.LibraryDtos
+namespace Jellyfin.Api.Models.LibraryDtos;
+
+/// <summary>
+/// Library options result dto.
+/// </summary>
+public class LibraryOptionsResultDto
{
/// <summary>
- /// Library options result dto.
+ /// Gets or sets the metadata savers.
/// </summary>
- public class LibraryOptionsResultDto
- {
- /// <summary>
- /// Gets or sets the metadata savers.
- /// </summary>
- public IReadOnlyList<LibraryOptionInfoDto> MetadataSavers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+ public IReadOnlyList<LibraryOptionInfoDto> MetadataSavers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
- /// <summary>
- /// Gets or sets the metadata readers.
- /// </summary>
- public IReadOnlyList<LibraryOptionInfoDto> MetadataReaders { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+ /// <summary>
+ /// Gets or sets the metadata readers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> MetadataReaders { get; set; } = Array.Empty<LibraryOptionInfoDto>();
- /// <summary>
- /// Gets or sets the subtitle fetchers.
- /// </summary>
- public IReadOnlyList<LibraryOptionInfoDto> SubtitleFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+ /// <summary>
+ /// Gets or sets the subtitle fetchers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> SubtitleFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
- /// <summary>
- /// Gets or sets the type options.
- /// </summary>
- public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
- }
+ /// <summary>
+ /// Gets or sets the type options.
+ /// </summary>
+ public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
index 20f45196d..125a6746e 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
@@ -3,36 +3,35 @@ using System.Collections.Generic;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
-namespace Jellyfin.Api.Models.LibraryDtos
+namespace Jellyfin.Api.Models.LibraryDtos;
+
+/// <summary>
+/// Library type options dto.
+/// </summary>
+public class LibraryTypeOptionsDto
{
/// <summary>
- /// Library type options dto.
+ /// Gets or sets the type.
/// </summary>
- public class LibraryTypeOptionsDto
- {
- /// <summary>
- /// Gets or sets the type.
- /// </summary>
- public string? Type { get; set; }
+ public string? Type { get; set; }
- /// <summary>
- /// Gets or sets the metadata fetchers.
- /// </summary>
- public IReadOnlyList<LibraryOptionInfoDto> MetadataFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+ /// <summary>
+ /// Gets or sets the metadata fetchers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> MetadataFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
- /// <summary>
- /// Gets or sets the image fetchers.
- /// </summary>
- public IReadOnlyList<LibraryOptionInfoDto> ImageFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+ /// <summary>
+ /// Gets or sets the image fetchers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> ImageFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
- /// <summary>
- /// Gets or sets the supported image types.
- /// </summary>
- public IReadOnlyList<ImageType> SupportedImageTypes { get; set; } = Array.Empty<ImageType>();
+ /// <summary>
+ /// Gets or sets the supported image types.
+ /// </summary>
+ public IReadOnlyList<ImageType> SupportedImageTypes { get; set; } = Array.Empty<ImageType>();
- /// <summary>
- /// Gets or sets the default image options.
- /// </summary>
- public IReadOnlyList<ImageOption> DefaultImageOptions { get; set; } = Array.Empty<ImageOption>();
- }
+ /// <summary>
+ /// Gets or sets the default image options.
+ /// </summary>
+ public IReadOnlyList<ImageOption> DefaultImageOptions { get; set; } = Array.Empty<ImageOption>();
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
index f93638898..b34e0bba5 100644
--- a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
@@ -1,16 +1,15 @@
using System;
using System.Collections.Generic;
-namespace Jellyfin.Api.Models.LibraryDtos
+namespace Jellyfin.Api.Models.LibraryDtos;
+
+/// <summary>
+/// Media Update Info Dto.
+/// </summary>
+public class MediaUpdateInfoDto
{
/// <summary>
- /// Media Update Info Dto.
+ /// Gets or sets the list of updates.
/// </summary>
- public class MediaUpdateInfoDto
- {
- /// <summary>
- /// Gets or sets the list of updates.
- /// </summary>
- public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
- }
+ public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
index 852315b92..5bbaea669 100644
--- a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
@@ -1,19 +1,18 @@
-namespace Jellyfin.Api.Models.LibraryDtos
+namespace Jellyfin.Api.Models.LibraryDtos;
+
+/// <summary>
+/// The media update info path.
+/// </summary>
+public class MediaUpdateInfoPathDto
{
/// <summary>
- /// The media update info path.
+ /// Gets or sets media path.
/// </summary>
- public class MediaUpdateInfoPathDto
- {
- /// <summary>
- /// Gets or sets media path.
- /// </summary>
- public string? Path { get; set; }
+ public string? Path { get; set; }
- /// <summary>
- /// Gets or sets media update type.
- /// Created, Modified, Deleted.
- /// </summary>
- public string? UpdateType { get; set; }
- }
+ /// <summary>
+ /// Gets or sets media update type.
+ /// Created, Modified, Deleted.
+ /// </summary>
+ public string? UpdateType { get; set; }
}
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs
index ab68d5223..16d3f65c9 100644
--- a/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs
+++ b/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs
@@ -1,15 +1,14 @@
using MediaBrowser.Model.Configuration;
-namespace Jellyfin.Api.Models.LibraryStructureDto
+namespace Jellyfin.Api.Models.LibraryStructureDto;
+
+/// <summary>
+/// Add virtual folder dto.
+/// </summary>
+public class AddVirtualFolderDto
{
/// <summary>
- /// Add virtual folder dto.
+ /// Gets or sets library options.
/// </summary>
- public class AddVirtualFolderDto
- {
- /// <summary>
- /// Gets or sets library options.
- /// </summary>
- public LibraryOptions? LibraryOptions { get; set; }
- }
+ public LibraryOptions? LibraryOptions { get; set; }
}
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
index 8b26ec317..94ffc5238 100644
--- a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
+++ b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
@@ -1,27 +1,26 @@
using System.ComponentModel.DataAnnotations;
using MediaBrowser.Model.Configuration;
-namespace Jellyfin.Api.Models.LibraryStructureDto
+namespace Jellyfin.Api.Models.LibraryStructureDto;
+
+/// <summary>
+/// Media Path dto.
+/// </summary>
+public class MediaPathDto
{
/// <summary>
- /// Media Path dto.
+ /// Gets or sets the name of the library.
/// </summary>
- public class MediaPathDto
- {
- /// <summary>
- /// Gets or sets the name of the library.
- /// </summary>
- [Required]
- public string? Name { get; set; }
+ [Required]
+ public string? Name { get; set; }
- /// <summary>
- /// Gets or sets the path to add.
- /// </summary>
- public string? Path { get; set; }
+ /// <summary>
+ /// Gets or sets the path to add.
+ /// </summary>
+ public string? Path { get; set; }
- /// <summary>
- /// Gets or sets the path info.
- /// </summary>
- public MediaPathInfo? PathInfo { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the path info.
+ /// </summary>
+ public MediaPathInfo? PathInfo { get; set; }
}
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs
index c78ed51f7..225c7c7bc 100644
--- a/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs
+++ b/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs
@@ -1,21 +1,20 @@
using System;
using MediaBrowser.Model.Configuration;
-namespace Jellyfin.Api.Models.LibraryStructureDto
+namespace Jellyfin.Api.Models.LibraryStructureDto;
+
+/// <summary>
+/// Update library options dto.
+/// </summary>
+public class UpdateLibraryOptionsDto
{
/// <summary>
- /// Update library options dto.
+ /// Gets or sets the library item id.
/// </summary>
- public class UpdateLibraryOptionsDto
- {
- /// <summary>
- /// Gets or sets the library item id.
- /// </summary>
- public Guid Id { get; set; }
+ public Guid Id { get; set; }
- /// <summary>
- /// Gets or sets library options.
- /// </summary>
- public LibraryOptions? LibraryOptions { get; set; }
- }
+ /// <summary>
+ /// Gets or sets library options.
+ /// </summary>
+ public LibraryOptions? LibraryOptions { get; set; }
}
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs
index fbd4985f9..a4d33f3b9 100644
--- a/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs
+++ b/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs
@@ -1,23 +1,22 @@
using System.ComponentModel.DataAnnotations;
using MediaBrowser.Model.Configuration;
-namespace Jellyfin.Api.Models.LibraryStructureDto
+namespace Jellyfin.Api.Models.LibraryStructureDto;
+
+/// <summary>
+/// Update library options dto.
+/// </summary>
+public class UpdateMediaPathRequestDto
{
/// <summary>
- /// Update library options dto.
+ /// Gets or sets the library name.
/// </summary>
- public class UpdateMediaPathRequestDto
- {
- /// <summary>
- /// Gets or sets the library name.
- /// </summary>
- [Required]
- public string Name { get; set; } = null!;
+ [Required]
+ public string Name { get; set; } = null!;
- /// <summary>
- /// Gets or sets library folder path information.
- /// </summary>
- [Required]
- public MediaPathInfo PathInfo { get; set; } = null!;
- }
+ /// <summary>
+ /// Gets or sets library folder path information.
+ /// </summary>
+ [Required]
+ public MediaPathInfo PathInfo { get; set; } = null!;
}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
index e293c461c..75222ed01 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
@@ -1,34 +1,32 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
-namespace Jellyfin.Api.Models.LiveTvDtos
+namespace Jellyfin.Api.Models.LiveTvDtos;
+
+/// <summary>
+/// Channel mapping options dto.
+/// </summary>
+public class ChannelMappingOptionsDto
{
/// <summary>
- /// Channel mapping options dto.
+ /// Gets or sets list of tuner channels.
/// </summary>
- public class ChannelMappingOptionsDto
- {
- /// <summary>
- /// Gets or sets list of tuner channels.
- /// </summary>
- required public IReadOnlyList<TunerChannelMapping> TunerChannels { get; set; }
+ required public IReadOnlyList<TunerChannelMapping> TunerChannels { get; set; }
- /// <summary>
- /// Gets or sets list of provider channels.
- /// </summary>
- required public IReadOnlyList<NameIdPair> ProviderChannels { get; set; }
+ /// <summary>
+ /// Gets or sets list of provider channels.
+ /// </summary>
+ required public IReadOnlyList<NameIdPair> ProviderChannels { get; set; }
- /// <summary>
- /// Gets or sets list of mappings.
- /// </summary>
- public IReadOnlyList<NameValuePair> Mappings { get; set; } = Array.Empty<NameValuePair>();
+ /// <summary>
+ /// Gets or sets list of mappings.
+ /// </summary>
+ public IReadOnlyList<NameValuePair> Mappings { get; set; } = Array.Empty<NameValuePair>();
- /// <summary>
- /// Gets or sets provider name.
- /// </summary>
- public string? ProviderName { get; set; }
- }
+ /// <summary>
+ /// Gets or sets provider name.
+ /// </summary>
+ public string? ProviderName { get; set; }
}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 411e4c550..5e7dd689e 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -6,174 +6,173 @@ using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
-namespace Jellyfin.Api.Models.LiveTvDtos
+namespace Jellyfin.Api.Models.LiveTvDtos;
+
+/// <summary>
+/// Get programs dto.
+/// </summary>
+public class GetProgramsDto
{
/// <summary>
- /// Get programs dto.
- /// </summary>
- public class GetProgramsDto
- {
- /// <summary>
- /// Gets or sets the channels to return guide information for.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>();
-
- /// <summary>
- /// Gets or sets optional. Filter by user id.
- /// </summary>
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the minimum premiere start date.
- /// Optional.
- /// </summary>
- public DateTime? MinStartDate { get; set; }
-
- /// <summary>
- /// Gets or sets filter by programs that have completed airing, or not.
- /// Optional.
- /// </summary>
- public bool? HasAired { get; set; }
-
- /// <summary>
- /// Gets or sets filter by programs that are currently airing, or not.
- /// Optional.
- /// </summary>
- public bool? IsAiring { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum premiere start date.
- /// Optional.
- /// </summary>
- public DateTime? MaxStartDate { get; set; }
-
- /// <summary>
- /// Gets or sets the minimum premiere end date.
- /// Optional.
- /// </summary>
- public DateTime? MinEndDate { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum premiere end date.
- /// Optional.
- /// </summary>
- public DateTime? MaxEndDate { get; set; }
-
- /// <summary>
- /// Gets or sets filter for movies.
- /// Optional.
- /// </summary>
- public bool? IsMovie { get; set; }
-
- /// <summary>
- /// Gets or sets filter for series.
- /// Optional.
- /// </summary>
- public bool? IsSeries { get; set; }
-
- /// <summary>
- /// Gets or sets filter for news.
- /// Optional.
- /// </summary>
- public bool? IsNews { get; set; }
-
- /// <summary>
- /// Gets or sets filter for kids.
- /// Optional.
- /// </summary>
- public bool? IsKids { get; set; }
-
- /// <summary>
- /// Gets or sets filter for sports.
- /// Optional.
- /// </summary>
- public bool? IsSports { get; set; }
-
- /// <summary>
- /// Gets or sets the record index to start at. All items with a lower index will be dropped from the results.
- /// Optional.
- /// </summary>
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum number of records to return.
- /// Optional.
- /// </summary>
- public int? Limit { get; set; }
-
- /// <summary>
- /// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
- /// Optional.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<string> SortBy { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets sort Order - Ascending,Descending.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<SortOrder> SortOrder { get; set; } = Array.Empty<SortOrder>();
-
- /// <summary>
- /// Gets or sets the genres to return guide information for.
- /// </summary>
- [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
- public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets the genre ids to return guide information for.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>();
-
- /// <summary>
- /// Gets or sets include image information in output.
- /// Optional.
- /// </summary>
- public bool? EnableImages { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether retrieve total record count.
- /// </summary>
- public bool EnableTotalRecordCount { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the max number of images to return, per image type.
- /// Optional.
- /// </summary>
- public int? ImageTypeLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the image types to include in the output.
- /// Optional.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<ImageType> EnableImageTypes { get; set; } = Array.Empty<ImageType>();
-
- /// <summary>
- /// Gets or sets include user data.
- /// Optional.
- /// </summary>
- public bool? EnableUserData { get; set; }
-
- /// <summary>
- /// Gets or sets filter by series timer id.
- /// Optional.
- /// </summary>
- public string? SeriesTimerId { get; set; }
-
- /// <summary>
- /// Gets or sets filter by library series id.
- /// Optional.
- /// </summary>
- public Guid LibrarySeriesId { get; set; }
-
- /// <summary>
- /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
- /// Optional.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<ItemFields> Fields { get; set; } = Array.Empty<ItemFields>();
- }
+ /// Gets or sets the channels to return guide information for.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>();
+
+ /// <summary>
+ /// Gets or sets optional. Filter by user id.
+ /// </summary>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum premiere start date.
+ /// Optional.
+ /// </summary>
+ public DateTime? MinStartDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter by programs that have completed airing, or not.
+ /// Optional.
+ /// </summary>
+ public bool? HasAired { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter by programs that are currently airing, or not.
+ /// Optional.
+ /// </summary>
+ public bool? IsAiring { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum premiere start date.
+ /// Optional.
+ /// </summary>
+ public DateTime? MaxStartDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum premiere end date.
+ /// Optional.
+ /// </summary>
+ public DateTime? MinEndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum premiere end date.
+ /// Optional.
+ /// </summary>
+ public DateTime? MaxEndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter for movies.
+ /// Optional.
+ /// </summary>
+ public bool? IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter for series.
+ /// Optional.
+ /// </summary>
+ public bool? IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter for news.
+ /// Optional.
+ /// </summary>
+ public bool? IsNews { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter for kids.
+ /// Optional.
+ /// </summary>
+ public bool? IsKids { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter for sports.
+ /// Optional.
+ /// </summary>
+ public bool? IsSports { get; set; }
+
+ /// <summary>
+ /// Gets or sets the record index to start at. All items with a lower index will be dropped from the results.
+ /// Optional.
+ /// </summary>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum number of records to return.
+ /// Optional.
+ /// </summary>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
+ /// Optional.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<string> SortBy { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets sort Order - Ascending,Descending.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<SortOrder> SortOrder { get; set; } = Array.Empty<SortOrder>();
+
+ /// <summary>
+ /// Gets or sets the genres to return guide information for.
+ /// </summary>
+ [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
+ public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the genre ids to return guide information for.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>();
+
+ /// <summary>
+ /// Gets or sets include image information in output.
+ /// Optional.
+ /// </summary>
+ public bool? EnableImages { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether retrieve total record count.
+ /// </summary>
+ public bool EnableTotalRecordCount { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the max number of images to return, per image type.
+ /// Optional.
+ /// </summary>
+ public int? ImageTypeLimit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image types to include in the output.
+ /// Optional.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<ImageType> EnableImageTypes { get; set; } = Array.Empty<ImageType>();
+
+ /// <summary>
+ /// Gets or sets include user data.
+ /// Optional.
+ /// </summary>
+ public bool? EnableUserData { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter by series timer id.
+ /// Optional.
+ /// </summary>
+ public string? SeriesTimerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets filter by library series id.
+ /// Optional.
+ /// </summary>
+ public Guid LibrarySeriesId { get; set; }
+
+ /// <summary>
+ /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
+ /// Optional.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<ItemFields> Fields { get; set; } = Array.Empty<ItemFields>();
}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs
index e7501bd9f..2dbaece5e 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs
@@ -1,28 +1,27 @@
using System.ComponentModel.DataAnnotations;
-namespace Jellyfin.Api.Models.LiveTvDtos
+namespace Jellyfin.Api.Models.LiveTvDtos;
+
+/// <summary>
+/// Set channel mapping dto.
+/// </summary>
+public class SetChannelMappingDto
{
/// <summary>
- /// Set channel mapping dto.
+ /// Gets or sets the provider id.
/// </summary>
- public class SetChannelMappingDto
- {
- /// <summary>
- /// Gets or sets the provider id.
- /// </summary>
- [Required]
- public string ProviderId { get; set; } = string.Empty;
+ [Required]
+ public string ProviderId { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets the tuner channel id.
- /// </summary>
- [Required]
- public string TunerChannelId { get; set; } = string.Empty;
+ /// <summary>
+ /// Gets or sets the tuner channel id.
+ /// </summary>
+ [Required]
+ public string TunerChannelId { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets the provider channel id.
- /// </summary>
- [Required]
- public string ProviderChannelId { get; set; } = string.Empty;
- }
+ /// <summary>
+ /// Gets or sets the provider channel id.
+ /// </summary>
+ [Required]
+ public string ProviderChannelId { get; set; } = string.Empty;
}
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
index 704542326..99b3f7020 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -3,76 +3,75 @@ using System.Collections.Generic;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
-namespace Jellyfin.Api.Models.MediaInfoDtos
+namespace Jellyfin.Api.Models.MediaInfoDtos;
+
+/// <summary>
+/// Open live stream dto.
+/// </summary>
+public class OpenLiveStreamDto
{
/// <summary>
- /// Open live stream dto.
+ /// Gets or sets the open token.
/// </summary>
- public class OpenLiveStreamDto
- {
- /// <summary>
- /// Gets or sets the open token.
- /// </summary>
- public string? OpenToken { get; set; }
+ public string? OpenToken { get; set; }
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- public Guid? UserId { get; set; }
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ public Guid? UserId { get; set; }
- /// <summary>
- /// Gets or sets the play session id.
- /// </summary>
- public string? PlaySessionId { get; set; }
+ /// <summary>
+ /// Gets or sets the play session id.
+ /// </summary>
+ public string? PlaySessionId { get; set; }
- /// <summary>
- /// Gets or sets the max streaming bitrate.
- /// </summary>
- public int? MaxStreamingBitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the max streaming bitrate.
+ /// </summary>
+ public int? MaxStreamingBitrate { get; set; }
- /// <summary>
- /// Gets or sets the start time in ticks.
- /// </summary>
- public long? StartTimeTicks { get; set; }
+ /// <summary>
+ /// Gets or sets the start time in ticks.
+ /// </summary>
+ public long? StartTimeTicks { get; set; }
- /// <summary>
- /// Gets or sets the audio stream index.
- /// </summary>
- public int? AudioStreamIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the audio stream index.
+ /// </summary>
+ public int? AudioStreamIndex { get; set; }
- /// <summary>
- /// Gets or sets the subtitle stream index.
- /// </summary>
- public int? SubtitleStreamIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the subtitle stream index.
+ /// </summary>
+ public int? SubtitleStreamIndex { get; set; }
- /// <summary>
- /// Gets or sets the max audio channels.
- /// </summary>
- public int? MaxAudioChannels { get; set; }
+ /// <summary>
+ /// Gets or sets the max audio channels.
+ /// </summary>
+ public int? MaxAudioChannels { get; set; }
- /// <summary>
- /// Gets or sets the item id.
- /// </summary>
- public Guid? ItemId { get; set; }
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
+ public Guid? ItemId { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether to enable direct play.
- /// </summary>
- public bool? EnableDirectPlay { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable direct play.
+ /// </summary>
+ public bool? EnableDirectPlay { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether to enale direct stream.
- /// </summary>
- public bool? EnableDirectStream { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to enale direct stream.
+ /// </summary>
+ public bool? EnableDirectStream { get; set; }
- /// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public DeviceProfile? DeviceProfile { get; set; }
+ /// <summary>
+ /// Gets or sets the device profile.
+ /// </summary>
+ public DeviceProfile? DeviceProfile { get; set; }
- /// <summary>
- /// Gets or sets the device play protocols.
- /// </summary>
- public IReadOnlyList<MediaProtocol> DirectPlayProtocols { get; set; } = Array.Empty<MediaProtocol>();
- }
+ /// <summary>
+ /// Gets or sets the device play protocols.
+ /// </summary>
+ public IReadOnlyList<MediaProtocol> DirectPlayProtocols { get; set; } = Array.Empty<MediaProtocol>();
}
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
index c6bd5e56e..0ef1867cd 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
@@ -1,86 +1,85 @@
using System;
using MediaBrowser.Model.Dlna;
-namespace Jellyfin.Api.Models.MediaInfoDtos
+namespace Jellyfin.Api.Models.MediaInfoDtos;
+
+/// <summary>
+/// Plabyback info dto.
+/// </summary>
+public class PlaybackInfoDto
{
/// <summary>
- /// Plabyback info dto.
+ /// Gets or sets the playback userId.
+ /// </summary>
+ public Guid? UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the max streaming bitrate.
+ /// </summary>
+ public int? MaxStreamingBitrate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start time in ticks.
+ /// </summary>
+ public long? StartTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio stream index.
+ /// </summary>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle stream index.
+ /// </summary>
+ public int? SubtitleStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the max audio channels.
+ /// </summary>
+ public int? MaxAudioChannels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the media source id.
+ /// </summary>
+ public string? MediaSourceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the live stream id.
+ /// </summary>
+ public string? LiveStreamId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device profile.
+ /// </summary>
+ public DeviceProfile? DeviceProfile { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable direct play.
+ /// </summary>
+ public bool? EnableDirectPlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable direct stream.
+ /// </summary>
+ public bool? EnableDirectStream { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable transcoding.
+ /// </summary>
+ public bool? EnableTranscoding { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable video stream copy.
+ /// </summary>
+ public bool? AllowVideoStreamCopy { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to allow audio stream copy.
+ /// </summary>
+ public bool? AllowAudioStreamCopy { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to auto open the live stream.
/// </summary>
- public class PlaybackInfoDto
- {
- /// <summary>
- /// Gets or sets the playback userId.
- /// </summary>
- public Guid? UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the max streaming bitrate.
- /// </summary>
- public int? MaxStreamingBitrate { get; set; }
-
- /// <summary>
- /// Gets or sets the start time in ticks.
- /// </summary>
- public long? StartTimeTicks { get; set; }
-
- /// <summary>
- /// Gets or sets the audio stream index.
- /// </summary>
- public int? AudioStreamIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the subtitle stream index.
- /// </summary>
- public int? SubtitleStreamIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the max audio channels.
- /// </summary>
- public int? MaxAudioChannels { get; set; }
-
- /// <summary>
- /// Gets or sets the media source id.
- /// </summary>
- public string? MediaSourceId { get; set; }
-
- /// <summary>
- /// Gets or sets the live stream id.
- /// </summary>
- public string? LiveStreamId { get; set; }
-
- /// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public DeviceProfile? DeviceProfile { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable direct play.
- /// </summary>
- public bool? EnableDirectPlay { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable direct stream.
- /// </summary>
- public bool? EnableDirectStream { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable transcoding.
- /// </summary>
- public bool? EnableTranscoding { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable video stream copy.
- /// </summary>
- public bool? AllowVideoStreamCopy { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to allow audio stream copy.
- /// </summary>
- public bool? AllowAudioStreamCopy { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to auto open the live stream.
- /// </summary>
- public bool? AutoOpenLiveStream { get; set; }
- }
+ public bool? AutoOpenLiveStream { get; set; }
}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index 9060500c8..480ddab09 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -6,279 +6,278 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Models.PlaybackDtos
+namespace Jellyfin.Api.Models.PlaybackDtos;
+
+/// <summary>
+/// Class TranscodingJob.
+/// </summary>
+public class TranscodingJobDto : IDisposable
{
/// <summary>
- /// Class TranscodingJob.
+ /// The process lock.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1051:NoVisibleInstanceFields", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "SA1401:PrivateField", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")]
+ public readonly object ProcessLock = new object();
+
+ /// <summary>
+ /// Timer lock.
/// </summary>
- public class TranscodingJobDto : IDisposable
+ private readonly object _timerLock = new object();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingJobDto"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobDto}"/> interface.</param>
+ public TranscodingJobDto(ILogger<TranscodingJobDto> logger)
{
- /// <summary>
- /// The process lock.
- /// </summary>
- [SuppressMessage("Microsoft.Performance", "CA1051:NoVisibleInstanceFields", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "SA1401:PrivateField", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")]
- public readonly object ProcessLock = new object();
-
- /// <summary>
- /// Timer lock.
- /// </summary>
- private readonly object _timerLock = new object();
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TranscodingJobDto"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobDto}"/> interface.</param>
- public TranscodingJobDto(ILogger<TranscodingJobDto> logger)
- {
- Logger = logger;
- }
+ Logger = logger;
+ }
+
+ /// <summary>
+ /// Gets or sets the play session identifier.
+ /// </summary>
+ /// <value>The play session identifier.</value>
+ public string? PlaySessionId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the live stream identifier.
+ /// </summary>
+ /// <value>The live stream identifier.</value>
+ public string? LiveStreamId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether is live output.
+ /// </summary>
+ public bool IsLiveOutput { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public MediaSourceInfo? MediaSource { get; set; }
+
+ /// <summary>
+ /// Gets or sets path.
+ /// </summary>
+ public string? Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public TranscodingJobType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the process.
+ /// </summary>
+ /// <value>The process.</value>
+ public Process? Process { get; set; }
+
+ /// <summary>
+ /// Gets logger.
+ /// </summary>
+ public ILogger<TranscodingJobDto> Logger { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the active request count.
+ /// </summary>
+ /// <value>The active request count.</value>
+ public int ActiveRequestCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the kill timer.
+ /// </summary>
+ /// <value>The kill timer.</value>
+ private Timer? KillTimer { get; set; }
+
+ /// <summary>
+ /// Gets or sets device id.
+ /// </summary>
+ public string? DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets cancellation token source.
+ /// </summary>
+ public CancellationTokenSource? CancellationTokenSource { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether has exited.
+ /// </summary>
+ public bool HasExited { get; set; }
+
+ /// <summary>
+ /// Gets or sets exit code.
+ /// </summary>
+ public int ExitCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether is user paused.
+ /// </summary>
+ public bool IsUserPaused { get; set; }
+
+ /// <summary>
+ /// Gets or sets id.
+ /// </summary>
+ public string? Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets framerate.
+ /// </summary>
+ public float? Framerate { get; set; }
+
+ /// <summary>
+ /// Gets or sets completion percentage.
+ /// </summary>
+ public double? CompletionPercentage { get; set; }
- /// <summary>
- /// Gets or sets the play session identifier.
- /// </summary>
- /// <value>The play session identifier.</value>
- public string? PlaySessionId { get; set; }
-
- /// <summary>
- /// Gets or sets the live stream identifier.
- /// </summary>
- /// <value>The live stream identifier.</value>
- public string? LiveStreamId { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether is live output.
- /// </summary>
- public bool IsLiveOutput { get; set; }
-
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- public MediaSourceInfo? MediaSource { get; set; }
-
- /// <summary>
- /// Gets or sets path.
- /// </summary>
- public string? Path { get; set; }
-
- /// <summary>
- /// Gets or sets the type.
- /// </summary>
- /// <value>The type.</value>
- public TranscodingJobType Type { get; set; }
-
- /// <summary>
- /// Gets or sets the process.
- /// </summary>
- /// <value>The process.</value>
- public Process? Process { get; set; }
-
- /// <summary>
- /// Gets logger.
- /// </summary>
- public ILogger<TranscodingJobDto> Logger { get; private set; }
-
- /// <summary>
- /// Gets or sets the active request count.
- /// </summary>
- /// <value>The active request count.</value>
- public int ActiveRequestCount { get; set; }
-
- /// <summary>
- /// Gets or sets the kill timer.
- /// </summary>
- /// <value>The kill timer.</value>
- private Timer? KillTimer { get; set; }
-
- /// <summary>
- /// Gets or sets device id.
- /// </summary>
- public string? DeviceId { get; set; }
-
- /// <summary>
- /// Gets or sets cancellation token source.
- /// </summary>
- public CancellationTokenSource? CancellationTokenSource { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether has exited.
- /// </summary>
- public bool HasExited { get; set; }
-
- /// <summary>
- /// Gets or sets exit code.
- /// </summary>
- public int ExitCode { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether is user paused.
- /// </summary>
- public bool IsUserPaused { get; set; }
-
- /// <summary>
- /// Gets or sets id.
- /// </summary>
- public string? Id { get; set; }
-
- /// <summary>
- /// Gets or sets framerate.
- /// </summary>
- public float? Framerate { get; set; }
-
- /// <summary>
- /// Gets or sets completion percentage.
- /// </summary>
- public double? CompletionPercentage { get; set; }
-
- /// <summary>
- /// Gets or sets bytes downloaded.
- /// </summary>
- public long BytesDownloaded { get; set; }
-
- /// <summary>
- /// Gets or sets bytes transcoded.
- /// </summary>
- public long? BytesTranscoded { get; set; }
-
- /// <summary>
- /// Gets or sets bit rate.
- /// </summary>
- public int? BitRate { get; set; }
-
- /// <summary>
- /// Gets or sets transcoding position ticks.
- /// </summary>
- public long? TranscodingPositionTicks { get; set; }
-
- /// <summary>
- /// Gets or sets download position ticks.
- /// </summary>
- public long? DownloadPositionTicks { get; set; }
-
- /// <summary>
- /// Gets or sets transcoding throttler.
- /// </summary>
- public TranscodingThrottler? TranscodingThrottler { get; set; }
-
- /// <summary>
- /// Gets or sets last ping date.
- /// </summary>
- public DateTime LastPingDate { get; set; }
-
- /// <summary>
- /// Gets or sets ping timeout.
- /// </summary>
- public int PingTimeout { get; set; }
-
- /// <summary>
- /// Stop kill timer.
- /// </summary>
- public void StopKillTimer()
+ /// <summary>
+ /// Gets or sets bytes downloaded.
+ /// </summary>
+ public long BytesDownloaded { get; set; }
+
+ /// <summary>
+ /// Gets or sets bytes transcoded.
+ /// </summary>
+ public long? BytesTranscoded { get; set; }
+
+ /// <summary>
+ /// Gets or sets bit rate.
+ /// </summary>
+ public int? BitRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets transcoding position ticks.
+ /// </summary>
+ public long? TranscodingPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets download position ticks.
+ /// </summary>
+ public long? DownloadPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets transcoding throttler.
+ /// </summary>
+ public TranscodingThrottler? TranscodingThrottler { get; set; }
+
+ /// <summary>
+ /// Gets or sets last ping date.
+ /// </summary>
+ public DateTime LastPingDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets ping timeout.
+ /// </summary>
+ public int PingTimeout { get; set; }
+
+ /// <summary>
+ /// Stop kill timer.
+ /// </summary>
+ public void StopKillTimer()
+ {
+ lock (_timerLock)
{
- lock (_timerLock)
- {
- KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
- }
+ KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}
+ }
- /// <summary>
- /// Dispose kill timer.
- /// </summary>
- public void DisposeKillTimer()
+ /// <summary>
+ /// Dispose kill timer.
+ /// </summary>
+ public void DisposeKillTimer()
+ {
+ lock (_timerLock)
{
- lock (_timerLock)
+ if (KillTimer is not null)
{
- if (KillTimer is not null)
- {
- KillTimer.Dispose();
- KillTimer = null;
- }
+ KillTimer.Dispose();
+ KillTimer = null;
}
}
+ }
+
+ /// <summary>
+ /// Start kill timer.
+ /// </summary>
+ /// <param name="callback">Callback action.</param>
+ public void StartKillTimer(Action<object?> callback)
+ {
+ StartKillTimer(callback, PingTimeout);
+ }
- /// <summary>
- /// Start kill timer.
- /// </summary>
- /// <param name="callback">Callback action.</param>
- public void StartKillTimer(Action<object?> callback)
+ /// <summary>
+ /// Start kill timer.
+ /// </summary>
+ /// <param name="callback">Callback action.</param>
+ /// <param name="intervalMs">Callback interval.</param>
+ public void StartKillTimer(Action<object?> callback, int intervalMs)
+ {
+ if (HasExited)
{
- StartKillTimer(callback, PingTimeout);
+ return;
}
- /// <summary>
- /// Start kill timer.
- /// </summary>
- /// <param name="callback">Callback action.</param>
- /// <param name="intervalMs">Callback interval.</param>
- public void StartKillTimer(Action<object?> callback, int intervalMs)
+ lock (_timerLock)
{
- if (HasExited)
+ if (KillTimer is null)
{
- return;
+ Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+ KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
}
-
- lock (_timerLock)
+ else
{
- if (KillTimer is null)
- {
- Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
- KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
- }
- else
- {
- Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
- KillTimer.Change(intervalMs, Timeout.Infinite);
- }
+ Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+ KillTimer.Change(intervalMs, Timeout.Infinite);
}
}
+ }
- /// <summary>
- /// Change kill timer if started.
- /// </summary>
- public void ChangeKillTimerIfStarted()
+ /// <summary>
+ /// Change kill timer if started.
+ /// </summary>
+ public void ChangeKillTimerIfStarted()
+ {
+ if (HasExited)
{
- if (HasExited)
- {
- return;
- }
+ return;
+ }
- lock (_timerLock)
+ lock (_timerLock)
+ {
+ if (KillTimer is not null)
{
- if (KillTimer is not null)
- {
- var intervalMs = PingTimeout;
+ var intervalMs = PingTimeout;
- Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
- KillTimer.Change(intervalMs, Timeout.Infinite);
- }
+ Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+ KillTimer.Change(intervalMs, Timeout.Infinite);
}
}
+ }
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
- /// <summary>
- /// Dispose all resources.
- /// </summary>
- /// <param name="disposing">Whether to dispose all resources.</param>
- protected virtual void Dispose(bool disposing)
+ /// <summary>
+ /// Dispose all resources.
+ /// </summary>
+ /// <param name="disposing">Whether to dispose all resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
{
- if (disposing)
- {
- Process?.Dispose();
- Process = null;
- KillTimer?.Dispose();
- KillTimer = null;
- CancellationTokenSource?.Dispose();
- CancellationTokenSource = null;
- TranscodingThrottler?.Dispose();
- TranscodingThrottler = null;
- }
+ Process?.Dispose();
+ Process = null;
+ KillTimer?.Dispose();
+ KillTimer = null;
+ CancellationTokenSource?.Dispose();
+ CancellationTokenSource = null;
+ TranscodingThrottler?.Dispose();
+ TranscodingThrottler = null;
}
}
}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index 9c4e377cd..b577c4ea6 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -7,214 +7,213 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Models.PlaybackDtos
+namespace Jellyfin.Api.Models.PlaybackDtos;
+
+/// <summary>
+/// Transcoding throttler.
+/// </summary>
+public class TranscodingThrottler : IDisposable
{
+ private readonly TranscodingJobDto _job;
+ private readonly ILogger<TranscodingThrottler> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IMediaEncoder _mediaEncoder;
+ private Timer? _timer;
+ private bool _isPaused;
+
/// <summary>
- /// Transcoding throttler.
+ /// Initializes a new instance of the <see cref="TranscodingThrottler"/> class.
/// </summary>
- public class TranscodingThrottler : IDisposable
+ /// <param name="job">Transcoding job dto.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
{
- private readonly TranscodingJobDto _job;
- private readonly ILogger<TranscodingThrottler> _logger;
- private readonly IConfigurationManager _config;
- private readonly IFileSystem _fileSystem;
- private readonly IMediaEncoder _mediaEncoder;
- private Timer? _timer;
- private bool _isPaused;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TranscodingThrottler"/> class.
- /// </summary>
- /// <param name="job">Transcoding job dto.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
- /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
- {
- _job = job;
- _logger = logger;
- _config = config;
- _fileSystem = fileSystem;
- _mediaEncoder = mediaEncoder;
- }
+ _job = job;
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ }
- /// <summary>
- /// Start timer.
- /// </summary>
- public void Start()
- {
- _timer = new Timer(TimerCallback, null, 5000, 5000);
- }
+ /// <summary>
+ /// Start timer.
+ /// </summary>
+ public void Start()
+ {
+ _timer = new Timer(TimerCallback, null, 5000, 5000);
+ }
- /// <summary>
- /// Unpause transcoding.
- /// </summary>
- /// <returns>A <see cref="Task"/>.</returns>
- public async Task UnpauseTranscoding()
+ /// <summary>
+ /// Unpause transcoding.
+ /// </summary>
+ /// <returns>A <see cref="Task"/>.</returns>
+ public async Task UnpauseTranscoding()
+ {
+ if (_isPaused)
{
- if (_isPaused)
- {
- _logger.LogDebug("Sending resume command to ffmpeg");
+ _logger.LogDebug("Sending resume command to ffmpeg");
- try
- {
- var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
- await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
- _isPaused = false;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error resuming transcoding");
- }
+ try
+ {
+ var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
+ await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
+ _isPaused = false;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error resuming transcoding");
}
}
+ }
- /// <summary>
- /// Stop throttler.
- /// </summary>
- /// <returns>A <see cref="Task"/>.</returns>
- public async Task Stop()
+ /// <summary>
+ /// Stop throttler.
+ /// </summary>
+ /// <returns>A <see cref="Task"/>.</returns>
+ public async Task Stop()
+ {
+ DisposeTimer();
+ await UnpauseTranscoding().ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Dispose throttler.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose throttler.
+ /// </summary>
+ /// <param name="disposing">Disposing.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
{
DisposeTimer();
- await UnpauseTranscoding().ConfigureAwait(false);
}
+ }
- /// <summary>
- /// Dispose throttler.
- /// </summary>
- public void Dispose()
+ private EncodingOptions GetOptions()
+ {
+ return _config.GetEncodingOptions();
+ }
+
+ private async void TimerCallback(object? state)
+ {
+ if (_job.HasExited)
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ DisposeTimer();
+ return;
}
- /// <summary>
- /// Dispose throttler.
- /// </summary>
- /// <param name="disposing">Disposing.</param>
- protected virtual void Dispose(bool disposing)
+ var options = GetOptions();
+
+ if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
{
- if (disposing)
- {
- DisposeTimer();
- }
+ await PauseTranscoding().ConfigureAwait(false);
}
-
- private EncodingOptions GetOptions()
+ else
{
- return _config.GetEncodingOptions();
+ await UnpauseTranscoding().ConfigureAwait(false);
}
+ }
- private async void TimerCallback(object? state)
+ private async Task PauseTranscoding()
+ {
+ if (!_isPaused)
{
- if (_job.HasExited)
- {
- DisposeTimer();
- return;
- }
+ var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
- var options = GetOptions();
+ _logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
- if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
+ try
{
- await PauseTranscoding().ConfigureAwait(false);
+ await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
+ _isPaused = true;
}
- else
+ catch (Exception ex)
{
- await UnpauseTranscoding().ConfigureAwait(false);
+ _logger.LogError(ex, "Error pausing transcoding");
}
}
+ }
- private async Task PauseTranscoding()
+ private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds)
+ {
+ var bytesDownloaded = job.BytesDownloaded;
+ var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
+ var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
+
+ var path = job.Path ?? throw new ArgumentException("Path can't be null.");
+
+ var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
+
+ if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
{
- if (!_isPaused)
- {
- var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
+ // HLS - time-based consideration
- _logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
+ var targetGap = gapLengthInTicks;
+ var gap = transcodingPositionTicks - downloadPositionTicks;
- try
- {
- await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
- _isPaused = true;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error pausing transcoding");
- }
+ if (gap < targetGap)
+ {
+ _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
+ return false;
}
+
+ _logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
+ return true;
}
- private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds)
+ if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
{
- var bytesDownloaded = job.BytesDownloaded;
- var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
- var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
-
- var path = job.Path ?? throw new ArgumentException("Path can't be null.");
-
- var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
+ // Progressive Streaming - byte-based consideration
- if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
+ try
{
- // HLS - time-based consideration
+ var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
- var targetGap = gapLengthInTicks;
- var gap = transcodingPositionTicks - downloadPositionTicks;
+ // Estimate the bytes the transcoder should be ahead
+ double gapFactor = gapLengthInTicks;
+ gapFactor /= transcodingPositionTicks;
+ var targetGap = bytesTranscoded * gapFactor;
+
+ var gap = bytesTranscoded - bytesDownloaded;
if (gap < targetGap)
{
- _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
+ _logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
return false;
}
- _logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
+ _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
return true;
}
-
- if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
+ catch (Exception ex)
{
- // Progressive Streaming - byte-based consideration
-
- try
- {
- var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
-
- // Estimate the bytes the transcoder should be ahead
- double gapFactor = gapLengthInTicks;
- gapFactor /= transcodingPositionTicks;
- var targetGap = bytesTranscoded * gapFactor;
-
- var gap = bytesTranscoded - bytesDownloaded;
-
- if (gap < targetGap)
- {
- _logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
- return false;
- }
-
- _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
- return true;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting output size");
- return false;
- }
+ _logger.LogError(ex, "Error getting output size");
+ return false;
}
-
- _logger.LogDebug("No throttle data for {Path}", path);
- return false;
}
- private void DisposeTimer()
+ _logger.LogDebug("No throttle data for {Path}", path);
+ return false;
+ }
+
+ private void DisposeTimer()
+ {
+ if (_timer is not null)
{
- if (_timer is not null)
- {
- _timer.Dispose();
- _timer = null;
- }
+ _timer.Dispose();
+ _timer = null;
}
}
}
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 0761b2085..1fba32c5b 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -3,32 +3,31 @@ using System.Collections.Generic;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Json.Converters;
-namespace Jellyfin.Api.Models.PlaylistDtos
+namespace Jellyfin.Api.Models.PlaylistDtos;
+
+/// <summary>
+/// Create new playlist dto.
+/// </summary>
+public class CreatePlaylistDto
{
/// <summary>
- /// Create new playlist dto.
+ /// Gets or sets the name of the new playlist.
/// </summary>
- public class CreatePlaylistDto
- {
- /// <summary>
- /// Gets or sets the name of the new playlist.
- /// </summary>
- public string? Name { get; set; }
+ public string? Name { get; set; }
- /// <summary>
- /// Gets or sets item ids to add to the playlist.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
+ /// <summary>
+ /// Gets or sets item ids to add to the playlist.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- public Guid? UserId { get; set; }
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ public Guid? UserId { get; set; }
- /// <summary>
- /// Gets or sets the media type.
- /// </summary>
- public string? MediaType { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the media type.
+ /// </summary>
+ public string? MediaType { get; set; }
}
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
index fa62472e1..b88be33b2 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
@@ -5,84 +5,83 @@ using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Session;
-namespace Jellyfin.Api.Models.SessionDtos
+namespace Jellyfin.Api.Models.SessionDtos;
+
+/// <summary>
+/// Client capabilities dto.
+/// </summary>
+public class ClientCapabilitiesDto
{
/// <summary>
- /// Client capabilities dto.
+ /// Gets or sets the list of playable media types.
/// </summary>
- public class ClientCapabilitiesDto
- {
- /// <summary>
- /// Gets or sets the list of playable media types.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<string> PlayableMediaTypes { get; set; } = Array.Empty<string>();
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<string> PlayableMediaTypes { get; set; } = Array.Empty<string>();
- /// <summary>
- /// Gets or sets the list of supported commands.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = Array.Empty<GeneralCommandType>();
+ /// <summary>
+ /// Gets or sets the list of supported commands.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = Array.Empty<GeneralCommandType>();
- /// <summary>
- /// Gets or sets a value indicating whether session supports media control.
- /// </summary>
- public bool SupportsMediaControl { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether session supports media control.
+ /// </summary>
+ public bool SupportsMediaControl { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether session supports content uploading.
- /// </summary>
- public bool SupportsContentUploading { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether session supports content uploading.
+ /// </summary>
+ public bool SupportsContentUploading { get; set; }
- /// <summary>
- /// Gets or sets the message callback url.
- /// </summary>
- public string? MessageCallbackUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the message callback url.
+ /// </summary>
+ public string? MessageCallbackUrl { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether session supports a persistent identifier.
- /// </summary>
- public bool SupportsPersistentIdentifier { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether session supports a persistent identifier.
+ /// </summary>
+ public bool SupportsPersistentIdentifier { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether session supports sync.
- /// </summary>
- public bool SupportsSync { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether session supports sync.
+ /// </summary>
+ public bool SupportsSync { get; set; }
- /// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public DeviceProfile? DeviceProfile { get; set; }
+ /// <summary>
+ /// Gets or sets the device profile.
+ /// </summary>
+ public DeviceProfile? DeviceProfile { get; set; }
- /// <summary>
- /// Gets or sets the app store url.
- /// </summary>
- public string? AppStoreUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the app store url.
+ /// </summary>
+ public string? AppStoreUrl { get; set; }
- /// <summary>
- /// Gets or sets the icon url.
- /// </summary>
- public string? IconUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the icon url.
+ /// </summary>
+ public string? IconUrl { get; set; }
- /// <summary>
- /// Convert the dto to the full <see cref="ClientCapabilities"/> model.
- /// </summary>
- /// <returns>The converted <see cref="ClientCapabilities"/> model.</returns>
- public ClientCapabilities ToClientCapabilities()
+ /// <summary>
+ /// Convert the dto to the full <see cref="ClientCapabilities"/> model.
+ /// </summary>
+ /// <returns>The converted <see cref="ClientCapabilities"/> model.</returns>
+ public ClientCapabilities ToClientCapabilities()
+ {
+ return new ClientCapabilities
{
- return new ClientCapabilities
- {
- PlayableMediaTypes = PlayableMediaTypes,
- SupportedCommands = SupportedCommands,
- SupportsMediaControl = SupportsMediaControl,
- SupportsContentUploading = SupportsContentUploading,
- MessageCallbackUrl = MessageCallbackUrl,
- SupportsPersistentIdentifier = SupportsPersistentIdentifier,
- SupportsSync = SupportsSync,
- DeviceProfile = DeviceProfile,
- AppStoreUrl = AppStoreUrl,
- IconUrl = IconUrl
- };
- }
+ PlayableMediaTypes = PlayableMediaTypes,
+ SupportedCommands = SupportedCommands,
+ SupportsMediaControl = SupportsMediaControl,
+ SupportsContentUploading = SupportsContentUploading,
+ MessageCallbackUrl = MessageCallbackUrl,
+ SupportsPersistentIdentifier = SupportsPersistentIdentifier,
+ SupportsSync = SupportsSync,
+ DeviceProfile = DeviceProfile,
+ AppStoreUrl = AppStoreUrl,
+ IconUrl = IconUrl
+ };
}
}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
index a5f012245..402707819 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
@@ -1,23 +1,22 @@
-namespace Jellyfin.Api.Models.StartupDtos
+namespace Jellyfin.Api.Models.StartupDtos;
+
+/// <summary>
+/// The startup configuration DTO.
+/// </summary>
+public class StartupConfigurationDto
{
/// <summary>
- /// The startup configuration DTO.
+ /// Gets or sets UI language culture.
/// </summary>
- public class StartupConfigurationDto
- {
- /// <summary>
- /// Gets or sets UI language culture.
- /// </summary>
- public string? UICulture { get; set; }
+ public string? UICulture { get; set; }
- /// <summary>
- /// Gets or sets the metadata country code.
- /// </summary>
- public string? MetadataCountryCode { get; set; }
+ /// <summary>
+ /// Gets or sets the metadata country code.
+ /// </summary>
+ public string? MetadataCountryCode { get; set; }
- /// <summary>
- /// Gets or sets the preferred language for the metadata.
- /// </summary>
- public string? PreferredMetadataLanguage { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the preferred language for the metadata.
+ /// </summary>
+ public string? PreferredMetadataLanguage { get; set; }
}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
index 4027ba41a..0e7be24c4 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
@@ -1,22 +1,21 @@
using System.ComponentModel.DataAnnotations;
-namespace Jellyfin.Api.Models.StartupDtos
+namespace Jellyfin.Api.Models.StartupDtos;
+
+/// <summary>
+/// Startup remote access dto.
+/// </summary>
+public class StartupRemoteAccessDto
{
/// <summary>
- /// Startup remote access dto.
+ /// Gets or sets a value indicating whether enable remote access.
/// </summary>
- public class StartupRemoteAccessDto
- {
- /// <summary>
- /// Gets or sets a value indicating whether enable remote access.
- /// </summary>
- [Required]
- public bool EnableRemoteAccess { get; set; }
+ [Required]
+ public bool EnableRemoteAccess { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether enable automatic port mapping.
- /// </summary>
- [Required]
- public bool EnableAutomaticPortMapping { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether enable automatic port mapping.
+ /// </summary>
+ [Required]
+ public bool EnableAutomaticPortMapping { get; set; }
}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
index e4c973548..f473bbcef 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Models.StartupDtos
+namespace Jellyfin.Api.Models.StartupDtos;
+
+/// <summary>
+/// The startup user DTO.
+/// </summary>
+public class StartupUserDto
{
/// <summary>
- /// The startup user DTO.
+ /// Gets or sets the username.
/// </summary>
- public class StartupUserDto
- {
- /// <summary>
- /// Gets or sets the username.
- /// </summary>
- public string? Name { get; set; }
+ public string? Name { get; set; }
- /// <summary>
- /// Gets or sets the user's password.
- /// </summary>
- public string? Password { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the user's password.
+ /// </summary>
+ public string? Password { get; set; }
}
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
index 3791fadbe..4f1abb1ff 100644
--- a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
@@ -1,13 +1,12 @@
-namespace Jellyfin.Api.Models.StreamingDtos
+namespace Jellyfin.Api.Models.StreamingDtos;
+
+/// <summary>
+/// The hls video request dto.
+/// </summary>
+public class HlsAudioRequestDto : StreamingRequestDto
{
/// <summary>
- /// The hls video request dto.
+ /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
/// </summary>
- public class HlsAudioRequestDto : StreamingRequestDto
- {
- /// <summary>
- /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
- /// </summary>
- public bool EnableAdaptiveBitrateStreaming { get; set; }
- }
+ public bool EnableAdaptiveBitrateStreaming { get; set; }
}
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
index 7a4be091b..1cd3d0132 100644
--- a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
@@ -1,13 +1,12 @@
-namespace Jellyfin.Api.Models.StreamingDtos
+namespace Jellyfin.Api.Models.StreamingDtos;
+
+/// <summary>
+/// The hls video request dto.
+/// </summary>
+public class HlsVideoRequestDto : VideoRequestDto
{
/// <summary>
- /// The hls video request dto.
+ /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
/// </summary>
- public class HlsVideoRequestDto : VideoRequestDto
- {
- /// <summary>
- /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
- /// </summary>
- public bool EnableAdaptiveBitrateStreaming { get; set; }
- }
+ public bool EnableAdaptiveBitrateStreaming { get; set; }
}
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index 1fce1d20a..b75272d3f 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -5,192 +5,191 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
-namespace Jellyfin.Api.Models.StreamingDtos
+namespace Jellyfin.Api.Models.StreamingDtos;
+
+/// <summary>
+/// The stream state dto.
+/// </summary>
+public class StreamState : EncodingJobInfo, IDisposable
{
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StreamState" /> class.
+ /// </summary>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager" /> interface.</param>
+ /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param>
+ /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param>
+ public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper)
+ : base(transcodingType)
+ {
+ _mediaSourceManager = mediaSourceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ }
+
+ /// <summary>
+ /// Gets or sets the requested url.
+ /// </summary>
+ public string? RequestedUrl { get; set; }
+
/// <summary>
- /// The stream state dto.
+ /// Gets or sets the request.
/// </summary>
- public class StreamState : EncodingJobInfo, IDisposable
+ public StreamingRequestDto Request
{
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StreamState" /> class.
- /// </summary>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager" /> interface.</param>
- /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param>
- /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param>
- public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper)
- : base(transcodingType)
+ get => (StreamingRequestDto)BaseRequest;
+ set
{
- _mediaSourceManager = mediaSourceManager;
- _transcodingJobHelper = transcodingJobHelper;
+ BaseRequest = value;
+ IsVideoRequest = VideoRequest is not null;
}
+ }
+
+ /// <summary>
+ /// Gets the video request.
+ /// </summary>
+ public VideoRequestDto? VideoRequest => Request as VideoRequestDto;
+
+ /// <summary>
+ /// Gets or sets the direct stream provicer.
+ /// </summary>
+ /// <remarks>
+ /// Deprecated.
+ /// </remarks>
+ public IDirectStreamProvider? DirectStreamProvider { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path to wait for.
+ /// </summary>
+ public string? WaitForPath { get; set; }
- /// <summary>
- /// Gets or sets the requested url.
- /// </summary>
- public string? RequestedUrl { get; set; }
+ /// <summary>
+ /// Gets a value indicating whether the request outputs video.
+ /// </summary>
+ public bool IsOutputVideo => Request is VideoRequestDto;
- /// <summary>
- /// Gets or sets the request.
- /// </summary>
- public StreamingRequestDto Request
+ /// <summary>
+ /// Gets the segment length.
+ /// </summary>
+ public int SegmentLength
+ {
+ get
{
- get => (StreamingRequestDto)BaseRequest;
- set
+ if (Request.SegmentLength.HasValue)
{
- BaseRequest = value;
- IsVideoRequest = VideoRequest is not null;
+ return Request.SegmentLength.Value;
}
- }
- /// <summary>
- /// Gets the video request.
- /// </summary>
- public VideoRequestDto? VideoRequest => Request as VideoRequestDto;
-
- /// <summary>
- /// Gets or sets the direct stream provicer.
- /// </summary>
- /// <remarks>
- /// Deprecated.
- /// </remarks>
- public IDirectStreamProvider? DirectStreamProvider { get; set; }
-
- /// <summary>
- /// Gets or sets the path to wait for.
- /// </summary>
- public string? WaitForPath { get; set; }
-
- /// <summary>
- /// Gets a value indicating whether the request outputs video.
- /// </summary>
- public bool IsOutputVideo => Request is VideoRequestDto;
-
- /// <summary>
- /// Gets the segment length.
- /// </summary>
- public int SegmentLength
- {
- get
+ if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
- if (Request.SegmentLength.HasValue)
+ var userAgent = UserAgent ?? string.Empty;
+
+ if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1
+ || userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1
+ || userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1
+ || userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1
+ || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
{
- return Request.SegmentLength.Value;
+ return 6;
}
- if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
+ if (IsSegmentedLiveStream)
{
- var userAgent = UserAgent ?? string.Empty;
-
- if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return 6;
- }
-
- if (IsSegmentedLiveStream)
- {
- return 3;
- }
-
- return 6;
+ return 3;
}
- return 3;
+ return 6;
}
+
+ return 3;
}
+ }
- /// <summary>
- /// Gets the minimum number of segments.
- /// </summary>
- public int MinSegments
+ /// <summary>
+ /// Gets the minimum number of segments.
+ /// </summary>
+ public int MinSegments
+ {
+ get
{
- get
+ if (Request.MinSegments.HasValue)
{
- if (Request.MinSegments.HasValue)
- {
- return Request.MinSegments.Value;
- }
-
- return SegmentLength >= 10 ? 2 : 3;
+ return Request.MinSegments.Value;
}
- }
- /// <summary>
- /// Gets or sets the user agent.
- /// </summary>
- public string? UserAgent { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to estimate the content length.
- /// </summary>
- public bool EstimateContentLength { get; set; }
-
- /// <summary>
- /// Gets or sets the transcode seek info.
- /// </summary>
- public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable dlna headers.
- /// </summary>
- public bool EnableDlnaHeaders { get; set; }
-
- /// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public DeviceProfile? DeviceProfile { get; set; }
-
- /// <summary>
- /// Gets or sets the transcoding job.
- /// </summary>
- public TranscodingJobDto? TranscodingJob { get; set; }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
+ return SegmentLength >= 10 ? 2 : 3;
}
+ }
+
+ /// <summary>
+ /// Gets or sets the user agent.
+ /// </summary>
+ public string? UserAgent { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to estimate the content length.
+ /// </summary>
+ public bool EstimateContentLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transcode seek info.
+ /// </summary>
+ public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable dlna headers.
+ /// </summary>
+ public bool EnableDlnaHeaders { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device profile.
+ /// </summary>
+ public DeviceProfile? DeviceProfile { get; set; }
- /// <inheritdoc />
- public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
+ /// <summary>
+ /// Gets or sets the transcoding job.
+ /// </summary>
+ public TranscodingJobDto? TranscodingJob { get; set; }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <inheritdoc />
+ public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
+ {
+ _transcodingJobHelper.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
+ }
+
+ /// <summary>
+ /// Disposes the stream state.
+ /// </summary>
+ /// <param name="disposing">Whether the object is currently being disposed.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
{
- _transcodingJobHelper.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
+ return;
}
- /// <summary>
- /// Disposes the stream state.
- /// </summary>
- /// <param name="disposing">Whether the object is currently being disposed.</param>
- protected virtual void Dispose(bool disposing)
+ if (disposing)
{
- if (_disposed)
+ // REVIEW: Is this the right place for this?
+ if (MediaSource.RequiresClosing
+ && string.IsNullOrWhiteSpace(Request.LiveStreamId)
+ && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
{
- return;
- }
-
- if (disposing)
- {
- // REVIEW: Is this the right place for this?
- if (MediaSource.RequiresClosing
- && string.IsNullOrWhiteSpace(Request.LiveStreamId)
- && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
- {
- _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
- }
+ _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
}
+ }
- TranscodingJob = null;
+ TranscodingJob = null;
- _disposed = true;
- }
+ _disposed = true;
}
}
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
index f8b0212b6..389d6006d 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
@@ -1,55 +1,54 @@
using MediaBrowser.Controller.MediaEncoding;
-namespace Jellyfin.Api.Models.StreamingDtos
+namespace Jellyfin.Api.Models.StreamingDtos;
+
+/// <summary>
+/// The audio streaming request dto.
+/// </summary>
+public class StreamingRequestDto : BaseEncodingJobOptions
{
/// <summary>
- /// The audio streaming request dto.
- /// </summary>
- public class StreamingRequestDto : BaseEncodingJobOptions
- {
- /// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public string? DeviceProfileId { get; set; }
-
- /// <summary>
- /// Gets or sets the params.
- /// </summary>
- public string? Params { get; set; }
-
- /// <summary>
- /// Gets or sets the play session id.
- /// </summary>
- public string? PlaySessionId { get; set; }
-
- /// <summary>
- /// Gets or sets the tag.
- /// </summary>
- public string? Tag { get; set; }
-
- /// <summary>
- /// Gets or sets the segment container.
- /// </summary>
- public string? SegmentContainer { get; set; }
-
- /// <summary>
- /// Gets or sets the segment length.
- /// </summary>
- public int? SegmentLength { get; set; }
-
- /// <summary>
- /// Gets or sets the min segments.
- /// </summary>
- public int? MinSegments { get; set; }
-
- /// <summary>
- /// Gets or sets the position of the requested segment in ticks.
- /// </summary>
- public long CurrentRuntimeTicks { get; set; }
-
- /// <summary>
- /// Gets or sets the actual segment length in ticks.
- /// </summary>
- public long ActualSegmentLengthTicks { get; set; }
- }
+ /// Gets or sets the device profile.
+ /// </summary>
+ public string? DeviceProfileId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the params.
+ /// </summary>
+ public string? Params { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play session id.
+ /// </summary>
+ public string? PlaySessionId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tag.
+ /// </summary>
+ public string? Tag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the segment container.
+ /// </summary>
+ public string? SegmentContainer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the segment length.
+ /// </summary>
+ public int? SegmentLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the min segments.
+ /// </summary>
+ public int? MinSegments { get; set; }
+
+ /// <summary>
+ /// Gets or sets the position of the requested segment in ticks.
+ /// </summary>
+ public long CurrentRuntimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the actual segment length in ticks.
+ /// </summary>
+ public long ActualSegmentLengthTicks { get; set; }
}
diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
index cce2a89d4..60c529d4a 100644
--- a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
@@ -1,19 +1,18 @@
-namespace Jellyfin.Api.Models.StreamingDtos
+namespace Jellyfin.Api.Models.StreamingDtos;
+
+/// <summary>
+/// The video request dto.
+/// </summary>
+public class VideoRequestDto : StreamingRequestDto
{
/// <summary>
- /// The video request dto.
+ /// Gets a value indicating whether this instance has fixed resolution.
/// </summary>
- public class VideoRequestDto : StreamingRequestDto
- {
- /// <summary>
- /// Gets a value indicating whether this instance has fixed resolution.
- /// </summary>
- /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
- public bool HasFixedResolution => Width.HasValue || Height.HasValue;
+ /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
+ public bool HasFixedResolution => Width.HasValue || Height.HasValue;
- /// <summary>
- /// Gets or sets a value indicating whether to enable subtitles in the manifest.
- /// </summary>
- public bool EnableSubtitlesInManifest { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable subtitles in the manifest.
+ /// </summary>
+ public bool EnableSubtitlesInManifest { get; set; }
}
diff --git a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
index be0595798..3c903ea6b 100644
--- a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
+++ b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
@@ -1,34 +1,33 @@
using System.ComponentModel.DataAnnotations;
-namespace Jellyfin.Api.Models.SubtitleDtos
+namespace Jellyfin.Api.Models.SubtitleDtos;
+
+/// <summary>
+/// Upload subtitles dto.
+/// </summary>
+public class UploadSubtitleDto
{
/// <summary>
- /// Upload subtitles dto.
+ /// Gets or sets the subtitle language.
/// </summary>
- public class UploadSubtitleDto
- {
- /// <summary>
- /// Gets or sets the subtitle language.
- /// </summary>
- [Required]
- public string Language { get; set; } = string.Empty;
+ [Required]
+ public string Language { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets the subtitle format.
- /// </summary>
- [Required]
- public string Format { get; set; } = string.Empty;
+ /// <summary>
+ /// Gets or sets the subtitle format.
+ /// </summary>
+ [Required]
+ public string Format { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets a value indicating whether the subtitle is forced.
- /// </summary>
- [Required]
- public bool IsForced { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the subtitle is forced.
+ /// </summary>
+ [Required]
+ public bool IsForced { get; set; }
- /// <summary>
- /// Gets or sets the subtitle data.
- /// </summary>
- [Required]
- public string Data { get; set; } = string.Empty;
- }
+ /// <summary>
+ /// Gets or sets the subtitle data.
+ /// </summary>
+ [Required]
+ public string Data { get; set; } = string.Empty;
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/BufferRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/BufferRequestDto.cs
index 479c44084..e7613911e 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/BufferRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/BufferRequestDto.cs
@@ -1,42 +1,41 @@
using System;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class BufferRequestDto.
+/// </summary>
+public class BufferRequestDto
{
/// <summary>
- /// Class BufferRequestDto.
+ /// Initializes a new instance of the <see cref="BufferRequestDto"/> class.
/// </summary>
- public class BufferRequestDto
+ public BufferRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="BufferRequestDto"/> class.
- /// </summary>
- public BufferRequestDto()
- {
- PlaylistItemId = Guid.Empty;
- }
+ PlaylistItemId = Guid.Empty;
+ }
- /// <summary>
- /// Gets or sets when the request has been made by the client.
- /// </summary>
- /// <value>The date of the request.</value>
- public DateTime When { get; set; }
+ /// <summary>
+ /// Gets or sets when the request has been made by the client.
+ /// </summary>
+ /// <value>The date of the request.</value>
+ public DateTime When { get; set; }
- /// <summary>
- /// Gets or sets the position ticks.
- /// </summary>
- /// <value>The position ticks.</value>
- public long PositionTicks { get; set; }
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether the client playback is unpaused.
- /// </summary>
- /// <value>The client playback status.</value>
- public bool IsPlaying { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the client playback is unpaused.
+ /// </summary>
+ /// <value>The client playback status.</value>
+ public bool IsPlaying { get; set; }
- /// <summary>
- /// Gets or sets the playlist item identifier of the playing item.
- /// </summary>
- /// <value>The playlist item identifier.</value>
- public Guid PlaylistItemId { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the playlist item identifier of the playing item.
+ /// </summary>
+ /// <value>The playlist item identifier.</value>
+ public Guid PlaylistItemId { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/IgnoreWaitRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/IgnoreWaitRequestDto.cs
index 4c30b7be4..8ccd831bd 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/IgnoreWaitRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/IgnoreWaitRequestDto.cs
@@ -1,14 +1,13 @@
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class IgnoreWaitRequestDto.
+/// </summary>
+public class IgnoreWaitRequestDto
{
/// <summary>
- /// Class IgnoreWaitRequestDto.
+ /// Gets or sets a value indicating whether the client should be ignored.
/// </summary>
- public class IgnoreWaitRequestDto
- {
- /// <summary>
- /// Gets or sets a value indicating whether the client should be ignored.
- /// </summary>
- /// <value>The client group-wait status.</value>
- public bool IgnoreWait { get; set; }
- }
+ /// <value>The client group-wait status.</value>
+ public bool IgnoreWait { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/JoinGroupRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/JoinGroupRequestDto.cs
index ed97b8d6a..89ba511af 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/JoinGroupRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/JoinGroupRequestDto.cs
@@ -1,16 +1,15 @@
using System;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class JoinGroupRequestDto.
+/// </summary>
+public class JoinGroupRequestDto
{
/// <summary>
- /// Class JoinGroupRequestDto.
+ /// Gets or sets the group identifier.
/// </summary>
- public class JoinGroupRequestDto
- {
- /// <summary>
- /// Gets or sets the group identifier.
- /// </summary>
- /// <value>The identifier of the group to join.</value>
- public Guid GroupId { get; set; }
- }
+ /// <value>The identifier of the group to join.</value>
+ public Guid GroupId { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/MovePlaylistItemRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/MovePlaylistItemRequestDto.cs
index 3af25f3e3..220d147f2 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/MovePlaylistItemRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/MovePlaylistItemRequestDto.cs
@@ -1,30 +1,29 @@
using System;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class MovePlaylistItemRequestDto.
+/// </summary>
+public class MovePlaylistItemRequestDto
{
/// <summary>
- /// Class MovePlaylistItemRequestDto.
+ /// Initializes a new instance of the <see cref="MovePlaylistItemRequestDto"/> class.
/// </summary>
- public class MovePlaylistItemRequestDto
+ public MovePlaylistItemRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="MovePlaylistItemRequestDto"/> class.
- /// </summary>
- public MovePlaylistItemRequestDto()
- {
- PlaylistItemId = Guid.Empty;
- }
+ PlaylistItemId = Guid.Empty;
+ }
- /// <summary>
- /// Gets or sets the playlist identifier of the item.
- /// </summary>
- /// <value>The playlist identifier of the item.</value>
- public Guid PlaylistItemId { get; set; }
+ /// <summary>
+ /// Gets or sets the playlist identifier of the item.
+ /// </summary>
+ /// <value>The playlist identifier of the item.</value>
+ public Guid PlaylistItemId { get; set; }
- /// <summary>
- /// Gets or sets the new position.
- /// </summary>
- /// <value>The new position.</value>
- public int NewIndex { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the new position.
+ /// </summary>
+ /// <value>The new position.</value>
+ public int NewIndex { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs
index 441d7be36..32a3bb444 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs
@@ -1,22 +1,21 @@
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class NewGroupRequestDto.
+/// </summary>
+public class NewGroupRequestDto
{
/// <summary>
- /// Class NewGroupRequestDto.
+ /// Initializes a new instance of the <see cref="NewGroupRequestDto"/> class.
/// </summary>
- public class NewGroupRequestDto
+ public NewGroupRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="NewGroupRequestDto"/> class.
- /// </summary>
- public NewGroupRequestDto()
- {
- GroupName = string.Empty;
- }
-
- /// <summary>
- /// Gets or sets the group name.
- /// </summary>
- /// <value>The name of the new group.</value>
- public string GroupName { get; set; }
+ GroupName = string.Empty;
}
+
+ /// <summary>
+ /// Gets or sets the group name.
+ /// </summary>
+ /// <value>The name of the new group.</value>
+ public string GroupName { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/NextItemRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/NextItemRequestDto.cs
index f59a93f13..b5223af5d 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/NextItemRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/NextItemRequestDto.cs
@@ -1,24 +1,23 @@
using System;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class NextItemRequestDto.
+/// </summary>
+public class NextItemRequestDto
{
/// <summary>
- /// Class NextItemRequestDto.
+ /// Initializes a new instance of the <see cref="NextItemRequestDto"/> class.
/// </summary>
- public class NextItemRequestDto
+ public NextItemRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="NextItemRequestDto"/> class.
- /// </summary>
- public NextItemRequestDto()
- {
- PlaylistItemId = Guid.Empty;
- }
-
- /// <summary>
- /// Gets or sets the playing item identifier.
- /// </summary>
- /// <value>The playing item identifier.</value>
- public Guid PlaylistItemId { get; set; }
+ PlaylistItemId = Guid.Empty;
}
+
+ /// <summary>
+ /// Gets or sets the playing item identifier.
+ /// </summary>
+ /// <value>The playing item identifier.</value>
+ public Guid PlaylistItemId { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/PingRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/PingRequestDto.cs
index c4ac06856..f13395057 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/PingRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/PingRequestDto.cs
@@ -1,14 +1,13 @@
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class PingRequestDto.
+/// </summary>
+public class PingRequestDto
{
/// <summary>
- /// Class PingRequestDto.
+ /// Gets or sets the ping time.
/// </summary>
- public class PingRequestDto
- {
- /// <summary>
- /// Gets or sets the ping time.
- /// </summary>
- /// <value>The ping time.</value>
- public long Ping { get; set; }
- }
+ /// <value>The ping time.</value>
+ public long Ping { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/PlayRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/PlayRequestDto.cs
index 844388cd9..e0edaf5e0 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/PlayRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/PlayRequestDto.cs
@@ -1,37 +1,36 @@
using System;
using System.Collections.Generic;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class PlayRequestDto.
+/// </summary>
+public class PlayRequestDto
{
/// <summary>
- /// Class PlayRequestDto.
+ /// Initializes a new instance of the <see cref="PlayRequestDto"/> class.
/// </summary>
- public class PlayRequestDto
+ public PlayRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="PlayRequestDto"/> class.
- /// </summary>
- public PlayRequestDto()
- {
- PlayingQueue = Array.Empty<Guid>();
- }
+ PlayingQueue = Array.Empty<Guid>();
+ }
- /// <summary>
- /// Gets or sets the playing queue.
- /// </summary>
- /// <value>The playing queue.</value>
- public IReadOnlyList<Guid> PlayingQueue { get; set; }
+ /// <summary>
+ /// Gets or sets the playing queue.
+ /// </summary>
+ /// <value>The playing queue.</value>
+ public IReadOnlyList<Guid> PlayingQueue { get; set; }
- /// <summary>
- /// Gets or sets the position of the playing item in the queue.
- /// </summary>
- /// <value>The playing item position.</value>
- public int PlayingItemPosition { get; set; }
+ /// <summary>
+ /// Gets or sets the position of the playing item in the queue.
+ /// </summary>
+ /// <value>The playing item position.</value>
+ public int PlayingItemPosition { get; set; }
- /// <summary>
- /// Gets or sets the start position ticks.
- /// </summary>
- /// <value>The start position ticks.</value>
- public long StartPositionTicks { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the start position ticks.
+ /// </summary>
+ /// <value>The start position ticks.</value>
+ public long StartPositionTicks { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/PreviousItemRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/PreviousItemRequestDto.cs
index 7fd4a49be..f52bd7f46 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/PreviousItemRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/PreviousItemRequestDto.cs
@@ -1,24 +1,23 @@
using System;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class PreviousItemRequestDto.
+/// </summary>
+public class PreviousItemRequestDto
{
/// <summary>
- /// Class PreviousItemRequestDto.
+ /// Initializes a new instance of the <see cref="PreviousItemRequestDto"/> class.
/// </summary>
- public class PreviousItemRequestDto
+ public PreviousItemRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="PreviousItemRequestDto"/> class.
- /// </summary>
- public PreviousItemRequestDto()
- {
- PlaylistItemId = Guid.Empty;
- }
-
- /// <summary>
- /// Gets or sets the playing item identifier.
- /// </summary>
- /// <value>The playing item identifier.</value>
- public Guid PlaylistItemId { get; set; }
+ PlaylistItemId = Guid.Empty;
}
+
+ /// <summary>
+ /// Gets or sets the playing item identifier.
+ /// </summary>
+ /// <value>The playing item identifier.</value>
+ public Guid PlaylistItemId { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/QueueRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/QueueRequestDto.cs
index 2b187f443..c2c2fba04 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/QueueRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/QueueRequestDto.cs
@@ -2,31 +2,30 @@ using System;
using System.Collections.Generic;
using MediaBrowser.Model.SyncPlay;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class QueueRequestDto.
+/// </summary>
+public class QueueRequestDto
{
/// <summary>
- /// Class QueueRequestDto.
+ /// Initializes a new instance of the <see cref="QueueRequestDto"/> class.
/// </summary>
- public class QueueRequestDto
+ public QueueRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="QueueRequestDto"/> class.
- /// </summary>
- public QueueRequestDto()
- {
- ItemIds = Array.Empty<Guid>();
- }
+ ItemIds = Array.Empty<Guid>();
+ }
- /// <summary>
- /// Gets or sets the items to enqueue.
- /// </summary>
- /// <value>The items to enqueue.</value>
- public IReadOnlyList<Guid> ItemIds { get; set; }
+ /// <summary>
+ /// Gets or sets the items to enqueue.
+ /// </summary>
+ /// <value>The items to enqueue.</value>
+ public IReadOnlyList<Guid> ItemIds { get; set; }
- /// <summary>
- /// Gets or sets the mode in which to add the new items.
- /// </summary>
- /// <value>The enqueue mode.</value>
- public GroupQueueMode Mode { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the mode in which to add the new items.
+ /// </summary>
+ /// <value>The enqueue mode.</value>
+ public GroupQueueMode Mode { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/ReadyRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/ReadyRequestDto.cs
index d9c193016..d8be75ef1 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/ReadyRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/ReadyRequestDto.cs
@@ -1,42 +1,41 @@
using System;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class ReadyRequest.
+/// </summary>
+public class ReadyRequestDto
{
/// <summary>
- /// Class ReadyRequest.
+ /// Initializes a new instance of the <see cref="ReadyRequestDto"/> class.
/// </summary>
- public class ReadyRequestDto
+ public ReadyRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="ReadyRequestDto"/> class.
- /// </summary>
- public ReadyRequestDto()
- {
- PlaylistItemId = Guid.Empty;
- }
+ PlaylistItemId = Guid.Empty;
+ }
- /// <summary>
- /// Gets or sets when the request has been made by the client.
- /// </summary>
- /// <value>The date of the request.</value>
- public DateTime When { get; set; }
+ /// <summary>
+ /// Gets or sets when the request has been made by the client.
+ /// </summary>
+ /// <value>The date of the request.</value>
+ public DateTime When { get; set; }
- /// <summary>
- /// Gets or sets the position ticks.
- /// </summary>
- /// <value>The position ticks.</value>
- public long PositionTicks { get; set; }
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether the client playback is unpaused.
- /// </summary>
- /// <value>The client playback status.</value>
- public bool IsPlaying { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the client playback is unpaused.
+ /// </summary>
+ /// <value>The client playback status.</value>
+ public bool IsPlaying { get; set; }
- /// <summary>
- /// Gets or sets the playlist item identifier of the playing item.
- /// </summary>
- /// <value>The playlist item identifier.</value>
- public Guid PlaylistItemId { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the playlist item identifier of the playing item.
+ /// </summary>
+ /// <value>The playlist item identifier.</value>
+ public Guid PlaylistItemId { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
index 226a584e1..2c7234272 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
@@ -1,37 +1,36 @@
using System;
using System.Collections.Generic;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class RemoveFromPlaylistRequestDto.
+/// </summary>
+public class RemoveFromPlaylistRequestDto
{
/// <summary>
- /// Class RemoveFromPlaylistRequestDto.
+ /// Initializes a new instance of the <see cref="RemoveFromPlaylistRequestDto"/> class.
/// </summary>
- public class RemoveFromPlaylistRequestDto
+ public RemoveFromPlaylistRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="RemoveFromPlaylistRequestDto"/> class.
- /// </summary>
- public RemoveFromPlaylistRequestDto()
- {
- PlaylistItemIds = Array.Empty<Guid>();
- }
+ PlaylistItemIds = Array.Empty<Guid>();
+ }
- /// <summary>
- /// Gets or sets the playlist identifiers of the items. Ignored when clearing the playlist.
- /// </summary>
- /// <value>The playlist identifiers of the items.</value>
- public IReadOnlyList<Guid> PlaylistItemIds { get; set; }
+ /// <summary>
+ /// Gets or sets the playlist identifiers of the items. Ignored when clearing the playlist.
+ /// </summary>
+ /// <value>The playlist identifiers of the items.</value>
+ public IReadOnlyList<Guid> PlaylistItemIds { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether the entire playlist should be cleared.
- /// </summary>
- /// <value>Whether the entire playlist should be cleared.</value>
- public bool ClearPlaylist { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the entire playlist should be cleared.
+ /// </summary>
+ /// <value>Whether the entire playlist should be cleared.</value>
+ public bool ClearPlaylist { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether the playing item should be removed as well. Used only when clearing the playlist.
- /// </summary>
- /// <value>Whether the playing item should be removed as well.</value>
- public bool ClearPlayingItem { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether the playing item should be removed as well. Used only when clearing the playlist.
+ /// </summary>
+ /// <value>Whether the playing item should be removed as well.</value>
+ public bool ClearPlayingItem { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/SeekRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/SeekRequestDto.cs
index b9af0be7f..f461417e9 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/SeekRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/SeekRequestDto.cs
@@ -1,14 +1,13 @@
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class SeekRequestDto.
+/// </summary>
+public class SeekRequestDto
{
/// <summary>
- /// Class SeekRequestDto.
+ /// Gets or sets the position ticks.
/// </summary>
- public class SeekRequestDto
- {
- /// <summary>
- /// Gets or sets the position ticks.
- /// </summary>
- /// <value>The position ticks.</value>
- public long PositionTicks { get; set; }
- }
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/SetPlaylistItemRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/SetPlaylistItemRequestDto.cs
index b937679fc..40e665039 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/SetPlaylistItemRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/SetPlaylistItemRequestDto.cs
@@ -1,24 +1,23 @@
using System;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class SetPlaylistItemRequestDto.
+/// </summary>
+public class SetPlaylistItemRequestDto
{
/// <summary>
- /// Class SetPlaylistItemRequestDto.
+ /// Initializes a new instance of the <see cref="SetPlaylistItemRequestDto"/> class.
/// </summary>
- public class SetPlaylistItemRequestDto
+ public SetPlaylistItemRequestDto()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="SetPlaylistItemRequestDto"/> class.
- /// </summary>
- public SetPlaylistItemRequestDto()
- {
- PlaylistItemId = Guid.Empty;
- }
-
- /// <summary>
- /// Gets or sets the playlist identifier of the playing item.
- /// </summary>
- /// <value>The playlist identifier of the playing item.</value>
- public Guid PlaylistItemId { get; set; }
+ PlaylistItemId = Guid.Empty;
}
+
+ /// <summary>
+ /// Gets or sets the playlist identifier of the playing item.
+ /// </summary>
+ /// <value>The playlist identifier of the playing item.</value>
+ public Guid PlaylistItemId { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/SetRepeatModeRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/SetRepeatModeRequestDto.cs
index e748fc3e0..387d1ea77 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/SetRepeatModeRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/SetRepeatModeRequestDto.cs
@@ -1,16 +1,15 @@
using MediaBrowser.Model.SyncPlay;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class SetRepeatModeRequestDto.
+/// </summary>
+public class SetRepeatModeRequestDto
{
/// <summary>
- /// Class SetRepeatModeRequestDto.
+ /// Gets or sets the repeat mode.
/// </summary>
- public class SetRepeatModeRequestDto
- {
- /// <summary>
- /// Gets or sets the repeat mode.
- /// </summary>
- /// <value>The repeat mode.</value>
- public GroupRepeatMode Mode { get; set; }
- }
+ /// <value>The repeat mode.</value>
+ public GroupRepeatMode Mode { get; set; }
}
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/SetShuffleModeRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/SetShuffleModeRequestDto.cs
index 0e427f4a4..a67e3958c 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/SetShuffleModeRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/SetShuffleModeRequestDto.cs
@@ -1,16 +1,15 @@
using MediaBrowser.Model.SyncPlay;
-namespace Jellyfin.Api.Models.SyncPlayDtos
+namespace Jellyfin.Api.Models.SyncPlayDtos;
+
+/// <summary>
+/// Class SetShuffleModeRequestDto.
+/// </summary>
+public class SetShuffleModeRequestDto
{
/// <summary>
- /// Class SetShuffleModeRequestDto.
+ /// Gets or sets the shuffle mode.
/// </summary>
- public class SetShuffleModeRequestDto
- {
- /// <summary>
- /// Gets or sets the shuffle mode.
- /// </summary>
- /// <value>The shuffle mode.</value>
- public GroupShuffleMode Mode { get; set; }
- }
+ /// <value>The shuffle mode.</value>
+ public GroupShuffleMode Mode { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
index 31208264f..70c18a98a 100644
--- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
+++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos;
+
+/// <summary>
+/// The authenticate user by name request body.
+/// </summary>
+public class AuthenticateUserByName
{
/// <summary>
- /// The authenticate user by name request body.
+ /// Gets or sets the username.
/// </summary>
- public class AuthenticateUserByName
- {
- /// <summary>
- /// Gets or sets the username.
- /// </summary>
- public string? Username { get; set; }
+ public string? Username { get; set; }
- /// <summary>
- /// Gets or sets the plain text password.
- /// </summary>
- public string? Pw { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the plain text password.
+ /// </summary>
+ public string? Pw { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
index 1c88d3628..6b6d9682b 100644
--- a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
+++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
@@ -1,18 +1,20 @@
-namespace Jellyfin.Api.Models.UserDtos
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.UserDtos;
+
+/// <summary>
+/// The create user by name request body.
+/// </summary>
+public class CreateUserByName
{
/// <summary>
- /// The create user by name request body.
+ /// Gets or sets the username.
/// </summary>
- public class CreateUserByName
- {
- /// <summary>
- /// Gets or sets the username.
- /// </summary>
- public string? Name { get; set; }
+ [Required]
+ required public string Name { get; set; }
- /// <summary>
- /// Gets or sets the password.
- /// </summary>
- public string? Password { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the password.
+ /// </summary>
+ public string? Password { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
index b31c6539c..a0631fd07 100644
--- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
+++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
@@ -1,16 +1,15 @@
using System.ComponentModel.DataAnnotations;
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos;
+
+/// <summary>
+/// Forgot Password request body DTO.
+/// </summary>
+public class ForgotPasswordDto
{
/// <summary>
- /// Forgot Password request body DTO.
+ /// Gets or sets the entered username to have its password reset.
/// </summary>
- public class ForgotPasswordDto
- {
- /// <summary>
- /// Gets or sets the entered username to have its password reset.
- /// </summary>
- [Required]
- public string? EnteredUsername { get; set; }
- }
+ [Required]
+ required public string EnteredUsername { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
index 62780e23c..79b8a5d63 100644
--- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
+++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
@@ -1,16 +1,15 @@
using System.ComponentModel.DataAnnotations;
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos;
+
+/// <summary>
+/// Forgot Password Pin enter request body DTO.
+/// </summary>
+public class ForgotPasswordPinDto
{
/// <summary>
- /// Forgot Password Pin enter request body DTO.
+ /// Gets or sets the entered pin to have the password reset.
/// </summary>
- public class ForgotPasswordPinDto
- {
- /// <summary>
- /// Gets or sets the entered pin to have the password reset.
- /// </summary>
- [Required]
- public string? Pin { get; set; }
- }
+ [Required]
+ required public string Pin { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
index 9493c08c2..245002f80 100644
--- a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
+++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
@@ -1,16 +1,15 @@
using System.ComponentModel.DataAnnotations;
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos;
+
+/// <summary>
+/// The quick connect request body.
+/// </summary>
+public class QuickConnectDto
{
/// <summary>
- /// The quick connect request body.
+ /// Gets or sets the quick connect secret.
/// </summary>
- public class QuickConnectDto
- {
- /// <summary>
- /// Gets or sets the quick connect secret.
- /// </summary>
- [Required]
- public string Secret { get; set; } = null!;
- }
+ [Required]
+ public string Secret { get; set; } = null!;
}
diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
index 0a173ea1a..80b6203bc 100644
--- a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
+++ b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
@@ -1,23 +1,22 @@
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos;
+
+/// <summary>
+/// The update user easy password request body.
+/// </summary>
+public class UpdateUserEasyPassword
{
/// <summary>
- /// The update user easy password request body.
+ /// Gets or sets the new sha1-hashed password.
/// </summary>
- public class UpdateUserEasyPassword
- {
- /// <summary>
- /// Gets or sets the new sha1-hashed password.
- /// </summary>
- public string? NewPassword { get; set; }
+ public string? NewPassword { get; set; }
- /// <summary>
- /// Gets or sets the new password.
- /// </summary>
- public string? NewPw { get; set; }
+ /// <summary>
+ /// Gets or sets the new password.
+ /// </summary>
+ public string? NewPw { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether to reset the password.
- /// </summary>
- public bool ResetPassword { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether to reset the password.
+ /// </summary>
+ public bool ResetPassword { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs
index 8288dbbc4..5347fcc9a 100644
--- a/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs
+++ b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs
@@ -1,28 +1,27 @@
-namespace Jellyfin.Api.Models.UserDtos
+namespace Jellyfin.Api.Models.UserDtos;
+
+/// <summary>
+/// The update user password request body.
+/// </summary>
+public class UpdateUserPassword
{
/// <summary>
- /// The update user password request body.
+ /// Gets or sets the current sha1-hashed password.
/// </summary>
- public class UpdateUserPassword
- {
- /// <summary>
- /// Gets or sets the current sha1-hashed password.
- /// </summary>
- public string? CurrentPassword { get; set; }
+ public string? CurrentPassword { get; set; }
- /// <summary>
- /// Gets or sets the current plain text password.
- /// </summary>
- public string? CurrentPw { get; set; }
+ /// <summary>
+ /// Gets or sets the current plain text password.
+ /// </summary>
+ public string? CurrentPw { get; set; }
- /// <summary>
- /// Gets or sets the new plain text password.
- /// </summary>
- public string? NewPw { get; set; }
+ /// <summary>
+ /// Gets or sets the new plain text password.
+ /// </summary>
+ public string? NewPw { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether to reset the password.
- /// </summary>
- public bool ResetPassword { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether to reset the password.
+ /// </summary>
+ public bool ResetPassword { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
index 84b6b0958..314b6a324 100644
--- a/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
+++ b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
@@ -1,18 +1,17 @@
-namespace Jellyfin.Api.Models.UserViewDtos
+namespace Jellyfin.Api.Models.UserViewDtos;
+
+/// <summary>
+/// Special view option dto.
+/// </summary>
+public class SpecialViewOptionDto
{
/// <summary>
- /// Special view option dto.
+ /// Gets or sets view option name.
/// </summary>
- public class SpecialViewOptionDto
- {
- /// <summary>
- /// Gets or sets view option name.
- /// </summary>
- public string? Name { get; set; }
+ public string? Name { get; set; }
- /// <summary>
- /// Gets or sets view option id.
- /// </summary>
- public string? Id { get; set; }
- }
+ /// <summary>
+ /// Gets or sets view option id.
+ /// </summary>
+ public string? Id { get; set; }
}
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 288e03fcf..4a5e0ecd4 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -6,59 +6,58 @@ using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.WebSocketListeners
+namespace Jellyfin.Api.WebSocketListeners;
+
+/// <summary>
+/// Class SessionInfoWebSocketListener.
+/// </summary>
+public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
{
/// <summary>
- /// Class SessionInfoWebSocketListener.
+ /// The _kernel.
/// </summary>
- public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
- {
- /// <summary>
- /// The _kernel.
- /// </summary>
- private readonly IActivityManager _activityManager;
+ private readonly IActivityManager _activityManager;
- /// <summary>
- /// Initializes a new instance of the <see cref="ActivityLogWebSocketListener"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{ActivityLogWebSocketListener}"/> interface.</param>
- /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
- public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager)
- : base(logger)
- {
- _activityManager = activityManager;
- _activityManager.EntryCreated += OnEntryCreated;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ActivityLogWebSocketListener"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{ActivityLogWebSocketListener}"/> interface.</param>
+ /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+ public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager)
+ : base(logger)
+ {
+ _activityManager = activityManager;
+ _activityManager.EntryCreated += OnEntryCreated;
+ }
- /// <inheritdoc />
- protected override SessionMessageType Type => SessionMessageType.ActivityLogEntry;
+ /// <inheritdoc />
+ protected override SessionMessageType Type => SessionMessageType.ActivityLogEntry;
- /// <inheritdoc />
- protected override SessionMessageType StartType => SessionMessageType.ActivityLogEntryStart;
+ /// <inheritdoc />
+ protected override SessionMessageType StartType => SessionMessageType.ActivityLogEntryStart;
- /// <inheritdoc />
- protected override SessionMessageType StopType => SessionMessageType.ActivityLogEntryStop;
+ /// <inheritdoc />
+ protected override SessionMessageType StopType => SessionMessageType.ActivityLogEntryStop;
- /// <summary>
- /// Gets the data to send.
- /// </summary>
- /// <returns>Task{SystemInfo}.</returns>
- protected override Task<ActivityLogEntry[]> GetDataToSend()
- {
- return Task.FromResult(Array.Empty<ActivityLogEntry>());
- }
+ /// <summary>
+ /// Gets the data to send.
+ /// </summary>
+ /// <returns>Task{SystemInfo}.</returns>
+ protected override Task<ActivityLogEntry[]> GetDataToSend()
+ {
+ return Task.FromResult(Array.Empty<ActivityLogEntry>());
+ }
- /// <inheritdoc />
- protected override void Dispose(bool dispose)
- {
- _activityManager.EntryCreated -= OnEntryCreated;
+ /// <inheritdoc />
+ protected override void Dispose(bool dispose)
+ {
+ _activityManager.EntryCreated -= OnEntryCreated;
- base.Dispose(dispose);
- }
+ base.Dispose(dispose);
+ }
- private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
- {
- SendData(true).GetAwaiter().GetResult();
- }
+ private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
+ {
+ await SendData(true).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 94df23e56..a9df2d671 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -7,78 +7,77 @@ using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.WebSocketListeners
+namespace Jellyfin.Api.WebSocketListeners;
+
+/// <summary>
+/// Class ScheduledTasksWebSocketListener.
+/// </summary>
+public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<TaskInfo>, WebSocketListenerState>
{
/// <summary>
- /// Class ScheduledTasksWebSocketListener.
+ /// Gets or sets the task manager.
/// </summary>
- public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<TaskInfo>, WebSocketListenerState>
- {
- /// <summary>
- /// Gets or sets the task manager.
- /// </summary>
- /// <value>The task manager.</value>
- private readonly ITaskManager _taskManager;
+ /// <value>The task manager.</value>
+ private readonly ITaskManager _taskManager;
- /// <summary>
- /// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{ScheduledTasksWebSocketListener}"/> interface.</param>
- /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
- public ScheduledTasksWebSocketListener(ILogger<ScheduledTasksWebSocketListener> logger, ITaskManager taskManager)
- : base(logger)
- {
- _taskManager = taskManager;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{ScheduledTasksWebSocketListener}"/> interface.</param>
+ /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
+ public ScheduledTasksWebSocketListener(ILogger<ScheduledTasksWebSocketListener> logger, ITaskManager taskManager)
+ : base(logger)
+ {
+ _taskManager = taskManager;
- _taskManager.TaskExecuting += OnTaskExecuting;
- _taskManager.TaskCompleted += OnTaskCompleted;
- }
+ _taskManager.TaskExecuting += OnTaskExecuting;
+ _taskManager.TaskCompleted += OnTaskCompleted;
+ }
- /// <inheritdoc />
- protected override SessionMessageType Type => SessionMessageType.ScheduledTasksInfo;
+ /// <inheritdoc />
+ protected override SessionMessageType Type => SessionMessageType.ScheduledTasksInfo;
- /// <inheritdoc />
- protected override SessionMessageType StartType => SessionMessageType.ScheduledTasksInfoStart;
+ /// <inheritdoc />
+ protected override SessionMessageType StartType => SessionMessageType.ScheduledTasksInfoStart;
- /// <inheritdoc />
- protected override SessionMessageType StopType => SessionMessageType.ScheduledTasksInfoStop;
+ /// <inheritdoc />
+ protected override SessionMessageType StopType => SessionMessageType.ScheduledTasksInfoStop;
- /// <summary>
- /// Gets the data to send.
- /// </summary>
- /// <returns>Task{IEnumerable{TaskInfo}}.</returns>
- protected override Task<IEnumerable<TaskInfo>> GetDataToSend()
- {
- return Task.FromResult(_taskManager.ScheduledTasks
- .OrderBy(i => i.Name)
- .Select(ScheduledTaskHelpers.GetTaskInfo)
- .Where(i => !i.IsHidden));
- }
+ /// <summary>
+ /// Gets the data to send.
+ /// </summary>
+ /// <returns>Task{IEnumerable{TaskInfo}}.</returns>
+ protected override Task<IEnumerable<TaskInfo>> GetDataToSend()
+ {
+ return Task.FromResult(_taskManager.ScheduledTasks
+ .OrderBy(i => i.Name)
+ .Select(ScheduledTaskHelpers.GetTaskInfo)
+ .Where(i => !i.IsHidden));
+ }
- /// <inheritdoc />
- protected override void Dispose(bool dispose)
- {
- _taskManager.TaskExecuting -= OnTaskExecuting;
- _taskManager.TaskCompleted -= OnTaskCompleted;
+ /// <inheritdoc />
+ protected override void Dispose(bool dispose)
+ {
+ _taskManager.TaskExecuting -= OnTaskExecuting;
+ _taskManager.TaskCompleted -= OnTaskCompleted;
- base.Dispose(dispose);
- }
+ base.Dispose(dispose);
+ }
- private void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
- {
- SendData(true);
- e.Task.TaskProgress -= OnTaskProgress;
- }
+ private async void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
+ {
+ e.Task.TaskProgress -= OnTaskProgress;
+ await SendData(true).ConfigureAwait(false);
+ }
- private void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
- {
- SendData(true);
- e.Argument.TaskProgress += OnTaskProgress;
- }
+ private async void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
+ {
+ await SendData(true).ConfigureAwait(false);
+ e.Argument.TaskProgress += OnTaskProgress;
+ }
- private void OnTaskProgress(object? sender, GenericEventArgs<double> e)
- {
- SendData(false);
- }
+ private async void OnTaskProgress(object? sender, GenericEventArgs<double> e)
+ {
+ await SendData(false).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index d996ac69f..0d8bf205c 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -6,99 +6,98 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.WebSocketListeners
+namespace Jellyfin.Api.WebSocketListeners;
+
+/// <summary>
+/// Class SessionInfoWebSocketListener.
+/// </summary>
+public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
{
+ private readonly ISessionManager _sessionManager;
+
/// <summary>
- /// Class SessionInfoWebSocketListener.
+ /// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class.
/// </summary>
- public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
+ /// <param name="logger">Instance of the <see cref="ILogger{SessionInfoWebSocketListener}"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ public SessionInfoWebSocketListener(ILogger<SessionInfoWebSocketListener> logger, ISessionManager sessionManager)
+ : base(logger)
+ {
+ _sessionManager = sessionManager;
+
+ _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
+ }
+
+ /// <inheritdoc />
+ protected override SessionMessageType Type => SessionMessageType.Sessions;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StartType => SessionMessageType.SessionsStart;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StopType => SessionMessageType.SessionsStop;
+
+ /// <summary>
+ /// Gets the data to send.
+ /// </summary>
+ /// <returns>Task{SystemInfo}.</returns>
+ protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
+ {
+ return Task.FromResult(_sessionManager.Sessions);
+ }
+
+ /// <inheritdoc />
+ protected override void Dispose(bool dispose)
+ {
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+
+ base.Dispose(dispose);
+ }
+
+ private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
+ {
+ await SendData(false).ConfigureAwait(false);
+ }
+
+ private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
+ {
+ await SendData(true).ConfigureAwait(false);
+ }
+
+ private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
+ {
+ await SendData(!e.IsAutomated).ConfigureAwait(false);
+ }
+
+ private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
+ {
+ await SendData(true).ConfigureAwait(false);
+ }
+
+ private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
+ {
+ await SendData(true).ConfigureAwait(false);
+ }
+
+ private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
+ {
+ await SendData(true).ConfigureAwait(false);
+ }
+
+ private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
{
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{SessionInfoWebSocketListener}"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- public SessionInfoWebSocketListener(ILogger<SessionInfoWebSocketListener> logger, ISessionManager sessionManager)
- : base(logger)
- {
- _sessionManager = sessionManager;
-
- _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
- _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
- _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
- _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
- _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
- _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
- _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
- }
-
- /// <inheritdoc />
- protected override SessionMessageType Type => SessionMessageType.Sessions;
-
- /// <inheritdoc />
- protected override SessionMessageType StartType => SessionMessageType.SessionsStart;
-
- /// <inheritdoc />
- protected override SessionMessageType StopType => SessionMessageType.SessionsStop;
-
- /// <summary>
- /// Gets the data to send.
- /// </summary>
- /// <returns>Task{SystemInfo}.</returns>
- protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
- {
- return Task.FromResult(_sessionManager.Sessions);
- }
-
- /// <inheritdoc />
- protected override void Dispose(bool dispose)
- {
- _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
- _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
- _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
- _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
- _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
- _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
- _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
-
- base.Dispose(dispose);
- }
-
- private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
- {
- await SendData(false).ConfigureAwait(false);
- }
-
- private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
- {
- await SendData(true).ConfigureAwait(false);
- }
-
- private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
- {
- await SendData(!e.IsAutomated).ConfigureAwait(false);
- }
-
- private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
- {
- await SendData(true).ConfigureAwait(false);
- }
-
- private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
- {
- await SendData(true).ConfigureAwait(false);
- }
-
- private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
- {
- await SendData(true).ConfigureAwait(false);
- }
-
- private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
- {
- await SendData(true).ConfigureAwait(false);
- }
+ await SendData(true).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs
index b7ba30180..82abfb831 100644
--- a/Jellyfin.Data/DayOfWeekHelper.cs
+++ b/Jellyfin.Data/DayOfWeekHelper.cs
@@ -17,5 +17,16 @@ namespace Jellyfin.Data
_ => new[] { (DayOfWeek)day }
};
}
+
+ public static bool Contains(this DynamicDayOfWeek dynamicDayOfWeek, DayOfWeek dayOfWeek)
+ {
+ return dynamicDayOfWeek switch
+ {
+ DynamicDayOfWeek.Everyday => true,
+ DynamicDayOfWeek.Weekday => dayOfWeek is >= DayOfWeek.Monday and <= DayOfWeek.Friday,
+ DynamicDayOfWeek.Weekend => dayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
+ _ => (DayOfWeek)dynamicDayOfWeek == dayOfWeek
+ };
+ }
}
}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index eb59e70f3..606e1b542 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -508,6 +508,7 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
+ Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
}
/// <summary>
@@ -525,8 +526,9 @@ namespace Jellyfin.Data.Entities
{
var localTime = date.ToLocalTime();
var hour = localTime.TimeOfDay.TotalHours;
+ var currentDayOfWeek = localTime.DayOfWeek;
- return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
+ return schedule.DayOfWeek.Contains(currentDayOfWeek)
&& hour >= schedule.StartHour
&& hour <= schedule.EndHour;
}
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 7d5200874..40280b95e 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -108,6 +108,11 @@ namespace Jellyfin.Data.Enums
/// <summary>
/// Whether the server should force transcoding on remote connections for the user.
/// </summary>
- ForceRemoteSourceTranscoding = 20
+ ForceRemoteSourceTranscoding = 20,
+
+ /// <summary>
+ /// Whether the user can create, modify and delete collections.
+ /// </summary>
+ EnableCollectionManagement = 21
}
}
diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs
index a54d789af..d2b412e45 100644
--- a/Jellyfin.Data/Enums/PreferenceKind.cs
+++ b/Jellyfin.Data/Enums/PreferenceKind.cs
@@ -63,6 +63,11 @@ namespace Jellyfin.Data.Enums
/// <summary>
/// A list of ordered views.
/// </summary>
- OrderedViews = 11
+ OrderedViews = 11,
+
+ /// <summary>
+ /// A list of allowed tags.
+ /// </summary>
+ AllowedTags = 12
}
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 7fe6466d4..1bc5d8bf9 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -24,22 +24,22 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 975d1c8ce..4cff5927f 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -11,13 +11,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 5520e2f04..f406e27a6 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -316,7 +316,7 @@ namespace Jellyfin.Networking.Manager
/// <inheritdoc/>
public string GetBindInterface(string source, out int? port)
{
- if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host))
+ if (IPHost.TryParse(source, out IPHost host))
{
return GetBindInterface(host, out port);
}
@@ -375,7 +375,7 @@ namespace Jellyfin.Networking.Manager
if (MatchesPublishedServerUrl(source, isExternal, out string res, out port))
{
- _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port);
+ _logger.LogDebug("{Source}: Using BindAddress {Address}:{Port}", source, res, port);
return res;
}
}
@@ -1019,8 +1019,8 @@ namespace Jellyfin.Networking.Manager
_internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork));
}
- _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString());
- _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString());
+ _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.AsString());
+ _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.AsString());
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString());
}
}
@@ -1145,7 +1145,7 @@ namespace Jellyfin.Networking.Manager
}
_logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count);
- _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString());
+ _logger.LogDebug("Interfaces addresses: {0}", _interfaceAddresses.AsString());
}
}
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 9d6ca6aab..ce1c54cbb 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity
/// </summary>
public class ActivityManager : IActivityManager
{
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// </summary>
/// <param name="provider">The Jellyfin database provider.</param>
- public ActivityManager(IDbContextFactory<JellyfinDb> provider)
+ public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
{
_provider = provider;
}
@@ -48,18 +48,10 @@ namespace Jellyfin.Server.Implementations.Activity
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- IQueryable<ActivityLog> entries = dbContext.ActivityLogs
- .OrderByDescending(entry => entry.DateCreated);
-
- if (query.MinDate.HasValue)
- {
- entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
- }
-
- if (query.HasUserId.HasValue)
- {
- entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
- }
+ var entries = dbContext.ActivityLogs
+ .OrderByDescending(entry => entry.DateCreated)
+ .Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate)
+ .Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value);
return new QueryResult<ActivityLogEntry>(
query.Skip,
@@ -67,8 +59,16 @@ namespace Jellyfin.Server.Implementations.Activity
await entries
.Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
- .AsAsyncEnumerable()
- .Select(ConvertToOldModel)
+ .Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId)
+ {
+ Id = entity.Id,
+ Overview = entity.Overview,
+ ShortOverview = entity.ShortOverview,
+ ItemId = entity.ItemId,
+ Date = entity.DateCreated,
+ Severity = entity.LogSeverity
+ })
+ .AsQueryable()
.ToListAsync()
.ConfigureAwait(false));
}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index 15ac5c668..a4b4c1959 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -9,6 +9,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using Jellyfin.Extensions;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Devices;
@@ -23,7 +24,7 @@ namespace Jellyfin.Server.Implementations.Devices
/// </summary>
public class DeviceManager : IDeviceManager
{
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IUserManager _userManager;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
@@ -32,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Devices
/// </summary>
/// <param name="dbProvider">The database provider.</param>
/// <param name="userManager">The user manager.</param>
- public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager)
+ public DeviceManager(IDbContextFactory<JellyfinDbContext> dbProvider, IUserManager userManager)
{
_dbProvider = dbProvider;
_userManager = userManager;
@@ -54,7 +55,7 @@ namespace Jellyfin.Server.Implementations.Devices
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
+ deviceOptions = await dbContext.DeviceOptions.FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
if (deviceOptions is null)
{
deviceOptions = new DeviceOptions(deviceId);
@@ -132,22 +133,11 @@ namespace Jellyfin.Server.Implementations.Devices
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var devices = dbContext.Devices.AsQueryable();
-
- if (query.UserId.HasValue)
- {
- devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
- }
-
- if (query.DeviceId is not null)
- {
- devices = devices.Where(device => device.DeviceId == query.DeviceId);
- }
-
- if (query.AccessToken is not null)
- {
- devices = devices.Where(device => device.AccessToken == query.AccessToken);
- }
+ var devices = dbContext.Devices
+ .OrderBy(d => d.Id)
+ .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
+ .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
+ .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
var count = await devices.CountAsync().ConfigureAwait(false);
@@ -179,11 +169,10 @@ namespace Jellyfin.Server.Implementations.Devices
/// <inheritdoc />
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
{
- IAsyncEnumerable<Device> sessions;
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- sessions = dbContext.Devices
+ IAsyncEnumerable<Device> sessions = dbContext.Devices
.Include(d => d.User)
.OrderByDescending(d => d.DateLastActivity)
.ThenBy(d => d.DeviceId)
@@ -197,6 +186,10 @@ namespace Jellyfin.Server.Implementations.Devices
if (userId.HasValue)
{
var user = _userManager.GetUserById(userId.Value);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
}
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index 05c622931..bb8d4dd14 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -4,7 +4,6 @@ using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.Extensions;
@@ -29,13 +28,11 @@ public static class ServiceCollectionExtensions
.SkipCachingResults(result =>
result.Value is null || (result.Value is EFTableRows rows && rows.RowsCount == 0)));
- serviceCollection.AddPooledDbContextFactory<JellyfinDb>((serviceProvider, opt) =>
+ serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
- var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
- .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>())
- .UseLoggerFactory(loggerFactory);
+ .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
});
return serviceCollection;
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index bc437c5d7..390ed58b3 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -8,13 +8,13 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@@ -22,15 +22,15 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.2" />
- <PackageReference Include="System.Linq.Async" Version="6.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
+ <PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
+ <PackageReference Include="System.Linq.Async" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
deleted file mode 100644
index dc4f53913..000000000
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ /dev/null
@@ -1,162 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Entities.Security;
-using Jellyfin.Data.Interfaces;
-using Microsoft.EntityFrameworkCore;
-
-namespace Jellyfin.Server.Implementations
-{
- /// <inheritdoc/>
- public class JellyfinDb : DbContext
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="JellyfinDb"/> class.
- /// </summary>
- /// <param name="options">The database context options.</param>
- public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
- {
- }
-
- /// <summary>
- /// Gets or sets the default connection string.
- /// </summary>
- public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
-
- public virtual DbSet<AccessSchedule> AccessSchedules { get; set; }
-
- public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
-
- public virtual DbSet<ApiKey> ApiKeys { get; set; }
-
- public virtual DbSet<Device> Devices { get; set; }
-
- public virtual DbSet<DeviceOptions> DeviceOptions { get; set; }
-
- public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
-
- public virtual DbSet<ImageInfo> ImageInfos { get; set; }
-
- public virtual DbSet<ItemDisplayPreferences> ItemDisplayPreferences { get; set; }
-
- public virtual DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences { get; set; }
-
- public virtual DbSet<Permission> Permissions { get; set; }
-
- public virtual DbSet<Preference> Preferences { get; set; }
-
- public virtual DbSet<User> Users { get; set; }
-
- /*public virtual DbSet<Artwork> Artwork { get; set; }
-
- public virtual DbSet<Book> Books { get; set; }
-
- public virtual DbSet<BookMetadata> BookMetadata { get; set; }
-
- public virtual DbSet<Chapter> Chapters { get; set; }
-
- public virtual DbSet<Collection> Collections { get; set; }
-
- public virtual DbSet<CollectionItem> CollectionItems { get; set; }
-
- public virtual DbSet<Company> Companies { get; set; }
-
- public virtual DbSet<CompanyMetadata> CompanyMetadata { get; set; }
-
- public virtual DbSet<CustomItem> CustomItems { get; set; }
-
- public virtual DbSet<CustomItemMetadata> CustomItemMetadata { get; set; }
-
- public virtual DbSet<Episode> Episodes { get; set; }
-
- public virtual DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
-
- public virtual DbSet<Genre> Genres { get; set; }
-
- public virtual DbSet<Group> Groups { get; set; }
-
- public virtual DbSet<Library> Libraries { get; set; }
-
- public virtual DbSet<LibraryItem> LibraryItems { get; set; }
-
- public virtual DbSet<LibraryRoot> LibraryRoot { get; set; }
-
- public virtual DbSet<MediaFile> MediaFiles { get; set; }
-
- public virtual DbSet<MediaFileStream> MediaFileStream { get; set; }
-
- public virtual DbSet<Metadata> Metadata { get; set; }
-
- public virtual DbSet<MetadataProvider> MetadataProviders { get; set; }
-
- public virtual DbSet<MetadataProviderId> MetadataProviderIds { get; set; }
-
- public virtual DbSet<Movie> Movies { get; set; }
-
- public virtual DbSet<MovieMetadata> MovieMetadata { get; set; }
-
- public virtual DbSet<MusicAlbum> MusicAlbums { get; set; }
-
- public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; }
-
- public virtual DbSet<Person> People { get; set; }
-
- public virtual DbSet<PersonRole> PersonRoles { get; set; }
-
- public virtual DbSet<Photo> Photo { get; set; }
-
- public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; }
-
- public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
-
- public virtual DbSet<Rating> Ratings { get; set; }
-
- /// <summary>
- /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
- /// store review ratings, not age ratings.
- /// </summary>
- public virtual DbSet<RatingSource> RatingSources { get; set; }
-
- public virtual DbSet<Release> Releases { get; set; }
-
- public virtual DbSet<Season> Seasons { get; set; }
-
- public virtual DbSet<SeasonMetadata> SeasonMetadata { get; set; }
-
- public virtual DbSet<Series> Series { get; set; }
-
- public virtual DbSet<SeriesMetadata> SeriesMetadata { get; set; }
-
- public virtual DbSet<Track> Tracks { get; set; }
-
- public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }*/
-
- /// <inheritdoc/>
- public override int SaveChanges()
- {
- foreach (var saveEntity in ChangeTracker.Entries()
- .Where(e => e.State == EntityState.Modified)
- .Select(entry => entry.Entity)
- .OfType<IHasConcurrencyToken>())
- {
- saveEntity.OnSavingChanges();
- }
-
- return base.SaveChanges();
- }
-
- /// <inheritdoc />
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
- base.OnModelCreating(modelBuilder);
- modelBuilder.HasDefaultSchema("jellyfin");
-
- // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
- modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly);
- }
- }
-}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
new file mode 100644
index 000000000..0d91707e3
--- /dev/null
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
+using Jellyfin.Data.Interfaces;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations;
+
+/// <inheritdoc/>
+public class JellyfinDbContext : DbContext
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
+ /// </summary>
+ /// <param name="options">The database context options.</param>
+ public JellyfinDbContext(DbContextOptions<JellyfinDbContext> options) : base(options)
+ {
+ }
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
+ /// </summary>
+ public DbSet<AccessSchedule> AccessSchedules => Set<AccessSchedule>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the activity logs.
+ /// </summary>
+ public DbSet<ActivityLog> ActivityLogs => Set<ActivityLog>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the API keys.
+ /// </summary>
+ public DbSet<ApiKey> ApiKeys => Set<ApiKey>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the devices.
+ /// </summary>
+ public DbSet<Device> Devices => Set<Device>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the device options.
+ /// </summary>
+ public DbSet<DeviceOptions> DeviceOptions => Set<DeviceOptions>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the display preferences.
+ /// </summary>
+ public DbSet<DisplayPreferences> DisplayPreferences => Set<DisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the image infos.
+ /// </summary>
+ public DbSet<ImageInfo> ImageInfos => Set<ImageInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the item display preferences.
+ /// </summary>
+ public DbSet<ItemDisplayPreferences> ItemDisplayPreferences => Set<ItemDisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the custom item display preferences.
+ /// </summary>
+ public DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences => Set<CustomItemDisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the permissions.
+ /// </summary>
+ public DbSet<Permission> Permissions => Set<Permission>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the preferences.
+ /// </summary>
+ public DbSet<Preference> Preferences => Set<Preference>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the users.
+ /// </summary>
+ public DbSet<User> Users => Set<User>();
+
+ /*public DbSet<Artwork> Artwork => Set<Artwork>();
+
+ public DbSet<Book> Books => Set<Book>();
+
+ public DbSet<BookMetadata> BookMetadata => Set<BookMetadata>();
+
+ public DbSet<Chapter> Chapters => Set<Chapter>();
+
+ public DbSet<Collection> Collections => Set<Collection>();
+
+ public DbSet<CollectionItem> CollectionItems => Set<CollectionItem>();
+
+ public DbSet<Company> Companies => Set<Company>();
+
+ public DbSet<CompanyMetadata> CompanyMetadata => Set<CompanyMetadata>();
+
+ public DbSet<CustomItem> CustomItems => Set<CustomItem>();
+
+ public DbSet<CustomItemMetadata> CustomItemMetadata => Set<CustomItemMetadata>();
+
+ public DbSet<Episode> Episodes => Set<Episode>();
+
+ public DbSet<EpisodeMetadata> EpisodeMetadata => Set<EpisodeMetadata>();
+
+ public DbSet<Genre> Genres => Set<Genre>();
+
+ public DbSet<Group> Groups => Set<Groups>();
+
+ public DbSet<Library> Libraries => Set<Library>();
+
+ public DbSet<LibraryItem> LibraryItems => Set<LibraryItems>();
+
+ public DbSet<LibraryRoot> LibraryRoot => Set<LibraryRoot>();
+
+ public DbSet<MediaFile> MediaFiles => Set<MediaFiles>();
+
+ public DbSet<MediaFileStream> MediaFileStream => Set<MediaFileStream>();
+
+ public DbSet<Metadata> Metadata => Set<Metadata>();
+
+ public DbSet<MetadataProvider> MetadataProviders => Set<MetadataProvider>();
+
+ public DbSet<MetadataProviderId> MetadataProviderIds => Set<MetadataProviderId>();
+
+ public DbSet<Movie> Movies => Set<Movie>();
+
+ public DbSet<MovieMetadata> MovieMetadata => Set<MovieMetadata>();
+
+ public DbSet<MusicAlbum> MusicAlbums => Set<MusicAlbum>();
+
+ public DbSet<MusicAlbumMetadata> MusicAlbumMetadata => Set<MusicAlbumMetadata>();
+
+ public DbSet<Person> People => Set<Person>();
+
+ public DbSet<PersonRole> PersonRoles => Set<PersonRole>();
+
+ public DbSet<Photo> Photo => Set<Photo>();
+
+ public DbSet<PhotoMetadata> PhotoMetadata => Set<PhotoMetadata>();
+
+ public DbSet<ProviderMapping> ProviderMappings => Set<ProviderMapping>();
+
+ public DbSet<Rating> Ratings => Set<Rating>();
+
+ /// <summary>
+ /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
+ /// store review ratings, not age ratings.
+ /// </summary>
+ public DbSet<RatingSource> RatingSources => Set<RatingSource>();
+
+ public DbSet<Release> Releases => Set<Release>();
+
+ public DbSet<Season> Seasons => Set<Season>();
+
+ public DbSet<SeasonMetadata> SeasonMetadata => Set<SeasonMetadata>();
+
+ public DbSet<Series> Series => Set<Series>();
+
+ public DbSet<SeriesMetadata> SeriesMetadata => Set<SeriesMetadata();
+
+ public DbSet<Track> Tracks => Set<Track>();
+
+ public DbSet<TrackMetadata> TrackMetadata => Set<TrackMetadata>();*/
+
+ /// <inheritdoc/>
+ public override int SaveChanges()
+ {
+ foreach (var saveEntity in ChangeTracker.Entries()
+ .Where(e => e.State == EntityState.Modified)
+ .Select(entry => entry.Entity)
+ .OfType<IHasConcurrencyToken>())
+ {
+ saveEntity.OnSavingChanges();
+ }
+
+ return base.SaveChanges();
+ }
+
+ /// <inheritdoc />
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
+ base.OnModelCreating(modelBuilder);
+
+ // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
index 98a83b745..4be6c2faa 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200514181226_AddActivityLog")]
partial class AddActivityLog
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
index 6342ce9cf..f3254734a 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200613202153_AddUsers")]
partial class AddUsers
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
index d44707d06..12d6faa8f 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200728005145_AddDisplayPreferences")]
partial class AddDisplayPreferences
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
index 2234f9d5f..f1cc20805 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200905220533_FixDisplayPreferencesIndex")]
partial class FixDisplayPreferencesIndex
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
index e5c326a32..f134d363c 100644
--- a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20201004171403_AddMaxActiveSessions")]
partial class AddMaxActiveSessions
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs
index 10663d065..ec65205d4 100644
--- a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20201204223655_AddCustomDisplayPreferences")]
partial class AddCustomDisplayPreferences
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
index 869676824..45dad6be6 100644
--- a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20210320181425_AddIndexesAndCollations")]
partial class AddIndexesAndCollations
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
index d332d19f2..eff84b457 100644
--- a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20210407110544_NullableCustomPrefValue")]
partial class NullableCustomPrefValue
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
index 7e9566e2e..ad7c2dd2c 100644
--- a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20210814002109_AddDevices")]
partial class AddDevices
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
index 03e3f3c92..f9497a3b6 100644
--- a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20221022080052_AddIndexActivityLogsDateCreated")]
partial class AddIndexActivityLogsDateCreated
{
diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
index 72a4a8c3b..940cf7c5d 100644
--- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
+++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
@@ -4,17 +4,17 @@ using Microsoft.EntityFrameworkCore.Design;
namespace Jellyfin.Server.Implementations.Migrations
{
/// <summary>
- /// The design time factory for <see cref="JellyfinDb"/>.
+ /// The design time factory for <see cref="JellyfinDbContext"/>.
/// This is only used for the creation of migrations and not during runtime.
/// </summary>
- internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDb>
+ internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDbContext>
{
- public JellyfinDb CreateDbContext(string[] args)
+ public JellyfinDbContext CreateDbContext(string[] args)
{
- var optionsBuilder = new DbContextOptionsBuilder<JellyfinDb>();
+ var optionsBuilder = new DbContextOptionsBuilder<JellyfinDbContext>();
optionsBuilder.UseSqlite("Data Source=jellyfin.db");
- return new JellyfinDb(optionsBuilder.Options);
+ return new JellyfinDbContext(optionsBuilder.Options);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 2dd7b094a..dd5f7f012 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
partial class JellyfinDbModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
index 810e57807..b2dfe60a1 100644
--- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
@@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security
/// <inheritdoc />
public class AuthenticationManager : IAuthenticationManager
{
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
/// </summary>
/// <param name="dbProvider">The database provider.</param>
- public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider)
+ public AuthenticationManager(IDbContextFactory<JellyfinDbContext> dbProvider)
{
_dbProvider = dbProvider;
}
@@ -40,7 +40,6 @@ namespace Jellyfin.Server.Implementations.Security
await using (dbContext.ConfigureAwait(false))
{
return await dbContext.ApiKeys
- .AsAsyncEnumerable()
.Select(key => new AuthenticationInfo
{
AppName = key.Name,
@@ -60,7 +59,6 @@ namespace Jellyfin.Server.Implementations.Security
await using (dbContext.ConfigureAwait(false))
{
var key = await dbContext.ApiKeys
- .AsQueryable()
.Where(apiKey => apiKey.AccessToken == accessToken)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index ec5742bab..63d3e8a04 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -16,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security
{
public class AuthorizationContext : IAuthorizationContext
{
- private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
private readonly IUserManager _userManager;
private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext(
- IDbContextFactory<JellyfinDb> jellyfinDb,
+ IDbContextFactory<JellyfinDbContext> jellyfinDb,
IUserManager userManager,
IServerApplicationHost serverApplicationHost)
{
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index fddad1c4f..bfae81e4c 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary>
public class DisplayPreferencesManager : IDisplayPreferencesManager
{
- private readonly JellyfinDb _dbContext;
+ private readonly JellyfinDbContext _dbContext;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
/// </summary>
/// <param name="dbContextFactory">The database context factory.</param>
- public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory)
+ public DisplayPreferencesManager(IDbContextFactory<JellyfinDbContext> dbContextFactory)
{
_dbContext = dbContextFactory.CreateDbContext();
}
@@ -62,7 +62,6 @@ namespace Jellyfin.Server.Implementations.Users
public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
{
return _dbContext.ItemDisplayPreferences
- .AsQueryable()
.Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && string.Equals(prefs.Client, client))
.ToList();
}
@@ -71,7 +70,6 @@ namespace Jellyfin.Server.Implementations.Users
public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
return _dbContext.CustomItemDisplayPreferences
- .AsQueryable()
.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& string.Equals(prefs.Client, client))
@@ -82,7 +80,6 @@ namespace Jellyfin.Server.Implementations.Users
public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences)
{
var existingPrefs = _dbContext.CustomItemDisplayPreferences
- .AsQueryable()
.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& string.Equals(prefs.Client, client));
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 19ac007b9..c4756433e 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary>
public class UserManager : IUserManager
{
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IEventManager _eventManager;
private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager;
@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
public UserManager(
- IDbContextFactory<JellyfinDb> dbProvider,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager,
ICryptoProvider cryptoProvider,
INetworkManager networkManager,
@@ -85,6 +85,7 @@ namespace Jellyfin.Server.Implementations.Users
_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateDbContext();
foreach (var user in dbContext.Users
+ .AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
@@ -143,7 +144,6 @@ namespace Jellyfin.Server.Implementations.Users
await using (dbContext.ConfigureAwait(false))
{
if (await dbContext.Users
- .AsQueryable()
.AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
.ConfigureAwait(false))
{
@@ -172,7 +172,7 @@ namespace Jellyfin.Server.Implementations.Users
}
}
- internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
+ internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
{
// TODO: Remove after user item data is migrated.
var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
@@ -369,8 +369,10 @@ namespace Jellyfin.Server.Implementations.Users
EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
+ EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
+ AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
@@ -684,6 +686,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+ user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
@@ -696,6 +699,7 @@ namespace Jellyfin.Server.Implementations.Users
// TODO: fix this at some point
user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+ user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
@@ -736,7 +740,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
}
- private static bool IsValidUsername(string name)
+ private static bool IsValidUsername(ReadOnlySpan<char> name)
{
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
@@ -886,7 +890,7 @@ namespace Jellyfin.Server.Implementations.Users
await UpdateUserAsync(user).ConfigureAwait(false);
}
- private async Task UpdateUserInternalAsync(JellyfinDb dbContext, User user)
+ private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{
dbContext.Users.Update(user);
_users[user.Id] = user;
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index d70b8f3ab..40cd5a044 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -107,7 +107,7 @@ namespace Jellyfin.Server
yield return typeof(CoreAppHost).Assembly;
// Jellyfin.Server.Implementations
- yield return typeof(JellyfinDb).Assembly;
+ yield return typeof(JellyfinDbContext).Assembly;
}
/// <inheritdoc />
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index e29167747..463ca7321 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
+using Jellyfin.Api.Middleware;
using Jellyfin.Networking.Configuration;
-using Jellyfin.Server.Middleware;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.OpenApi.Models;
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index f74152405..9867c9e47 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -5,28 +5,24 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
+using System.Security.Claims;
using Emby.Server.Implementations;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.AnonymousLanAccessPolicy;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
-using Jellyfin.Api.Auth.DownloadPolicy;
-using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
-using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
-using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
-using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Auth.FirstTimeSetupPolicy;
using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
-using Jellyfin.Api.Auth.LocalAccessPolicy;
-using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Auth.SyncPlayAccessPolicy;
+using Jellyfin.Api.Auth.UserPermissionPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
+using Jellyfin.Api.Formatters;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
-using Jellyfin.Server.Formatters;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
@@ -56,117 +52,38 @@ namespace Jellyfin.Server.Extensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
+ // The default handler must be first so that it is evaluated first
serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, DownloadHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrDefaultHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeOrIgnoreParentalControlSetupHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
+
return serviceCollection.AddAuthorizationCore(options =>
{
- options.AddPolicy(
- Policies.DefaultAuthorization,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new DefaultAuthorizationRequirement());
- });
- options.AddPolicy(
- Policies.Download,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new DownloadRequirement());
- });
- options.AddPolicy(
- Policies.FirstTimeSetupOrDefault,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement());
- });
- options.AddPolicy(
- Policies.FirstTimeSetupOrElevated,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
- });
- options.AddPolicy(
- Policies.IgnoreParentalControl,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new IgnoreParentalControlRequirement());
- });
- options.AddPolicy(
- Policies.FirstTimeSetupOrIgnoreParentalControl,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement());
- });
- options.AddPolicy(
- Policies.LocalAccessOnly,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new LocalAccessRequirement());
- });
- options.AddPolicy(
- Policies.LocalAccessOrRequiresElevation,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement());
- });
+ options.DefaultPolicy = new AuthorizationPolicyBuilder()
+ .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
+ .AddRequirements(new DefaultAuthorizationRequirement())
+ .Build();
+
+ options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement());
+ options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement));
+ options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading));
+ options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false));
+ options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement());
+ options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, false));
+ options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSchedule: false));
+ options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess));
+ options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement));
+ options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement());
+ options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
+ options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
+ options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
+ options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
options.AddPolicy(
Policies.RequiresElevation,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new RequiresElevationRequirement());
- });
- options.AddPolicy(
- Policies.SyncPlayHasAccess,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
- });
- options.AddPolicy(
- Policies.SyncPlayCreateGroup,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
- });
- options.AddPolicy(
- Policies.SyncPlayJoinGroup,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
- });
- options.AddPolicy(
- Policies.SyncPlayIsInGroup,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
- });
- options.AddPolicy(
- Policies.AnonymousLanAccessPolicy,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new AnonymousLanAccessRequirement());
- });
+ policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
+ .RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
});
}
@@ -334,6 +251,14 @@ namespace Jellyfin.Server.Extensions
});
}
+ private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)
+ {
+ authorizationOptions.AddPolicy(policyName, policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizationRequirement);
+ });
+ }
+
/// <summary>
/// Sets up the proxy configuration based on the addresses in <paramref name="allowedProxies"/>.
/// </summary>
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
new file mode 100644
index 000000000..58d3e1b2d
--- /dev/null
+++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
@@ -0,0 +1,90 @@
+using System;
+using System.IO;
+using System.Net;
+using Jellyfin.Server.Helpers;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Extensions;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Extensions;
+
+/// <summary>
+/// Extensions for configuring the web host builder.
+/// </summary>
+public static class WebHostBuilderExtensions
+{
+ /// <summary>
+ /// Configure the web host builder.
+ /// </summary>
+ /// <param name="builder">The builder to configure.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="startupConfig">The application configuration.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <param name="logger">The logger.</param>
+ /// <returns>The configured web host builder.</returns>
+ public static IWebHostBuilder ConfigureWebHostBuilder(
+ this IWebHostBuilder builder,
+ CoreAppHost appHost,
+ IConfiguration startupConfig,
+ IApplicationPaths appPaths,
+ ILogger logger)
+ {
+ return builder
+ .UseKestrel((builderContext, options) =>
+ {
+ var addresses = appHost.NetManager.GetAllBindInterfaces();
+
+ bool flagged = false;
+ foreach (IPObject netAdd in addresses)
+ {
+ logger.LogInformation("Kestrel listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All Addresses" : netAdd);
+ options.Listen(netAdd.Address, appHost.HttpPort);
+ if (appHost.ListenWithHttps)
+ {
+ options.Listen(
+ netAdd.Address,
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps(appHost.Certificate));
+ }
+ else if (builderContext.HostingEnvironment.IsDevelopment())
+ {
+ try
+ {
+ options.Listen(
+ netAdd.Address,
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps());
+ }
+ catch (InvalidOperationException)
+ {
+ if (!flagged)
+ {
+ logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted");
+ flagged = true;
+ }
+ }
+ }
+ }
+
+ // Bind to unix socket (only on unix systems)
+ if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix)
+ {
+ var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths);
+
+ // Workaround for https://github.com/aspnet/AspNetCore/issues/14134
+ if (File.Exists(socketPath))
+ {
+ File.Delete(socketPath);
+ }
+
+ options.ListenUnixSocket(socketPath);
+ logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
+ }
+ })
+ .UseStartup(_ => new Startup(appHost));
+ }
+}
diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
index 4af670e9a..fb9f6d0a6 100644
--- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
+++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
@@ -18,11 +18,17 @@ namespace Jellyfin.Server.Filters
{
var requiredScopes = new List<string>();
+ var requiresAuth = false;
// Add all method scopes.
foreach (var attribute in context.MethodInfo.GetCustomAttributes(true))
{
- if (attribute is AuthorizeAttribute authorizeAttribute
- && authorizeAttribute.Policy is not null
+ if (attribute is not AuthorizeAttribute authorizeAttribute)
+ {
+ continue;
+ }
+
+ requiresAuth = true;
+ if (authorizeAttribute.Policy is not null
&& !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal))
{
requiredScopes.Add(authorizeAttribute.Policy);
@@ -35,8 +41,13 @@ namespace Jellyfin.Server.Filters
{
foreach (var attribute in controllerAttributes)
{
- if (attribute is AuthorizeAttribute authorizeAttribute
- && authorizeAttribute.Policy is not null
+ if (attribute is not AuthorizeAttribute authorizeAttribute)
+ {
+ continue;
+ }
+
+ requiresAuth = true;
+ if (authorizeAttribute.Policy is not null
&& !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal))
{
requiredScopes.Add(authorizeAttribute.Policy);
@@ -44,35 +55,37 @@ namespace Jellyfin.Server.Filters
}
}
- if (requiredScopes.Count != 0)
+ if (!requiresAuth)
{
- if (!operation.Responses.ContainsKey("401"))
- {
- operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
- }
+ return;
+ }
- if (!operation.Responses.ContainsKey("403"))
- {
- operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
- }
+ if (!operation.Responses.ContainsKey("401"))
+ {
+ operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
+ }
- var scheme = new OpenApiSecurityScheme
+ if (!operation.Responses.ContainsKey("403"))
+ {
+ operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
+ }
+
+ var scheme = new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
{
- Reference = new OpenApiReference
- {
- Type = ReferenceType.SecurityScheme,
- Id = AuthenticationSchemes.CustomAuthentication
- }
- };
+ Type = ReferenceType.SecurityScheme,
+ Id = AuthenticationSchemes.CustomAuthentication
+ }
+ };
- operation.Security = new List<OpenApiSecurityRequirement>
+ operation.Security = new List<OpenApiSecurityRequirement>
+ {
+ new OpenApiSecurityRequirement
{
- new OpenApiSecurityRequirement
- {
- [scheme] = requiredScopes
- }
- };
- }
+ [scheme] = requiredScopes
+ }
+ };
}
}
}
diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
deleted file mode 100644
index ea8c5ecdb..000000000
--- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Jellyfin.Extensions.Json;
-using Microsoft.AspNetCore.Mvc.Formatters;
-using Microsoft.Net.Http.Headers;
-
-namespace Jellyfin.Server.Formatters
-{
- /// <summary>
- /// Camel Case Json Profile Formatter.
- /// </summary>
- public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
- /// </summary>
- public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions)
- {
- SupportedMediaTypes.Clear();
- SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
- }
- }
-}
diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
deleted file mode 100644
index fdaa48f84..000000000
--- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Formatters;
-
-namespace Jellyfin.Server.Formatters
-{
- /// <summary>
- /// Css output formatter.
- /// </summary>
- public class CssOutputFormatter : TextOutputFormatter
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
- /// </summary>
- public CssOutputFormatter()
- {
- SupportedMediaTypes.Add("text/css");
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
- /// <summary>
- /// Write context object to stream.
- /// </summary>
- /// <param name="context">Writer context.</param>
- /// <param name="selectedEncoding">Unused. Writer encoding.</param>
- /// <returns>Write stream task.</returns>
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- var stringResponse = context.Object?.ToString();
- return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
- }
- }
-}
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
deleted file mode 100644
index 03ca7dda7..000000000
--- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Net.Mime;
-using Jellyfin.Extensions.Json;
-using Microsoft.AspNetCore.Mvc.Formatters;
-using Microsoft.Net.Http.Headers;
-
-namespace Jellyfin.Server.Formatters
-{
- /// <summary>
- /// Pascal Case Json Profile Formatter.
- /// </summary>
- public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
- /// </summary>
- public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions)
- {
- SupportedMediaTypes.Clear();
- // Add application/json for default formatter
- SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json));
- SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType));
- }
- }
-}
diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
deleted file mode 100644
index 156368d69..000000000
--- a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Net.Mime;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Formatters;
-
-namespace Jellyfin.Server.Formatters
-{
- /// <summary>
- /// Xml output formatter.
- /// </summary>
- public class XmlOutputFormatter : TextOutputFormatter
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
- /// </summary>
- public XmlOutputFormatter()
- {
- SupportedMediaTypes.Clear();
- SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
- /// <inheritdoc />
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- var stringResponse = context.Object?.ToString();
- return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
- }
- }
-}
diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs
new file mode 100644
index 000000000..fda6e5465
--- /dev/null
+++ b/Jellyfin.Server/Helpers/StartupHelpers.cs
@@ -0,0 +1,303 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Text;
+using System.Threading.Tasks;
+using Emby.Server.Implementations;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using SQLitePCL;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace Jellyfin.Server.Helpers;
+
+/// <summary>
+/// A class containing helper methods for server startup.
+/// </summary>
+public static class StartupHelpers
+{
+ private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
+
+ /// <summary>
+ /// Logs relevant environment variables and information about the host.
+ /// </summary>
+ /// <param name="logger">The logger to use.</param>
+ /// <param name="appPaths">The application paths to use.</param>
+ public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
+ {
+ // Distinct these to prevent users from reporting problems that aren't actually problems
+ var commandLineArgs = Environment
+ .GetCommandLineArgs()
+ .Distinct();
+
+ // Get all relevant environment variables
+ var allEnvVars = Environment.GetEnvironmentVariables();
+ var relevantEnvVars = new Dictionary<object, object>();
+ foreach (var key in allEnvVars.Keys)
+ {
+ if (_relevantEnvVarPrefixes.Any(prefix => key.ToString()!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
+ {
+ relevantEnvVars.Add(key, allEnvVars[key]!);
+ }
+ }
+
+ logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
+ logger.LogInformation("Arguments: {Args}", commandLineArgs);
+ logger.LogInformation("Operating system: {OS}", RuntimeInformation.OSDescription);
+ logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
+ logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
+ logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
+ logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
+ logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
+ logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
+ logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
+ }
+
+ /// <summary>
+ /// Create the data, config and log paths from the variety of inputs(command line args,
+ /// environment variables) or decide on what default to use. For Windows it's %AppPath%
+ /// for everything else the
+ /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
+ /// is followed.
+ /// </summary>
+ /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
+ /// <returns><see cref="ServerApplicationPaths" />.</returns>
+ public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
+ {
+ // LocalApplicationData
+ // Windows: %LocalAppData%
+ // macOS: NSApplicationSupportDirectory
+ // UNIX: $XDG_DATA_HOME
+ var dataDir = options.DataDir
+ ?? Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR")
+ ?? Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "jellyfin");
+
+ var configDir = options.ConfigDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
+ if (configDir is null)
+ {
+ configDir = Path.Join(dataDir, "config");
+ if (options.DataDir is null
+ && !Directory.Exists(configDir)
+ && !OperatingSystem.IsWindows()
+ && !OperatingSystem.IsMacOS())
+ {
+ // UNIX: $XDG_CONFIG_HOME
+ configDir = Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "jellyfin");
+ }
+ }
+
+ var cacheDir = options.CacheDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
+ if (cacheDir is null)
+ {
+ if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
+ {
+ cacheDir = Path.Join(dataDir, "cache");
+ }
+ else
+ {
+ cacheDir = Path.Join(GetXdgCacheHome(), "jellyfin");
+ }
+ }
+
+ var webDir = options.WebDir ?? Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
+ if (webDir is null)
+ {
+ webDir = Path.Join(AppContext.BaseDirectory, "jellyfin-web");
+ }
+
+ var logDir = options.LogDir ?? Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
+ if (logDir is null)
+ {
+ logDir = Path.Join(dataDir, "log");
+ }
+
+ // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
+ dataDir = Path.GetFullPath(dataDir);
+ logDir = Path.GetFullPath(logDir);
+ configDir = Path.GetFullPath(configDir);
+ cacheDir = Path.GetFullPath(cacheDir);
+ webDir = Path.GetFullPath(webDir);
+
+ // Ensure the main folders exist before we continue
+ try
+ {
+ Directory.CreateDirectory(dataDir);
+ Directory.CreateDirectory(logDir);
+ Directory.CreateDirectory(configDir);
+ Directory.CreateDirectory(cacheDir);
+ }
+ catch (IOException ex)
+ {
+ Console.Error.WriteLine("Error whilst attempting to create folder");
+ Console.Error.WriteLine(ex.ToString());
+ Environment.Exit(1);
+ }
+
+ return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
+ }
+
+ private static string GetXdgCacheHome()
+ {
+ // $XDG_CACHE_HOME defines the base directory relative to which
+ // user specific non-essential data files should be stored.
+ var cacheHome = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
+
+ // If $XDG_CACHE_HOME is either not set or a relative path,
+ // a default equal to $HOME/.cache should be used.
+ if (cacheHome is null || !cacheHome.StartsWith('/'))
+ {
+ cacheHome = Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".cache");
+ }
+
+ return cacheHome;
+ }
+
+ /// <summary>
+ /// Gets the path for the unix socket Kestrel should bind to.
+ /// </summary>
+ /// <param name="startupConfig">The startup config.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <returns>The path for Kestrel to bind to.</returns>
+ public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths)
+ {
+ var socketPath = startupConfig.GetUnixSocketPath();
+
+ if (string.IsNullOrEmpty(socketPath))
+ {
+ const string SocketFile = "jellyfin.sock";
+
+ var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
+ if (xdgRuntimeDir is null)
+ {
+ // Fall back to config dir
+ socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, SocketFile);
+ }
+ else
+ {
+ socketPath = Path.Join(xdgRuntimeDir, SocketFile);
+ }
+ }
+
+ return socketPath;
+ }
+
+ /// <summary>
+ /// Sets the unix file permissions for Kestrel's socket file.
+ /// </summary>
+ /// <param name="startupConfig">The startup config.</param>
+ /// <param name="socketPath">The socket path.</param>
+ /// <param name="logger">The logger.</param>
+ [UnsupportedOSPlatform("windows")]
+ public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger)
+ {
+ var socketPerms = startupConfig.GetUnixSocketPermissions();
+
+ if (!string.IsNullOrEmpty(socketPerms))
+ {
+ File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8));
+ logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms);
+ }
+ }
+
+ /// <summary>
+ /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
+ /// already.
+ /// </summary>
+ /// <param name="appPaths">The application paths.</param>
+ /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
+ public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
+ {
+ // Do nothing if the config file already exists
+ string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault);
+ if (File.Exists(configPath))
+ {
+ return;
+ }
+
+ // Get a stream of the resource contents
+ // NOTE: The .csproj name is used instead of the assembly name in the resource path
+ const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
+ Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
+ ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
+ await using (resource.ConfigureAwait(false))
+ {
+ Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (dst.ConfigureAwait(false))
+ {
+ // Copy the resource contents to the expected file path for the config file
+ await resource.CopyToAsync(dst).ConfigureAwait(false);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Initialize Serilog using configuration and fall back to defaults on failure.
+ /// </summary>
+ /// <param name="configuration">The configuration object.</param>
+ /// <param name="appPaths">The application paths.</param>
+ public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
+ {
+ try
+ {
+ // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
+ Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration)
+ .Enrich.FromLogContext()
+ .Enrich.WithThreadId()
+ .CreateLogger();
+ }
+ catch (Exception ex)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console(
+ outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
+ formatProvider: CultureInfo.InvariantCulture)
+ .WriteTo.Async(x => x.File(
+ Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
+ rollingInterval: RollingInterval.Day,
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
+ formatProvider: CultureInfo.InvariantCulture,
+ encoding: Encoding.UTF8))
+ .Enrich.FromLogContext()
+ .Enrich.WithThreadId()
+ .CreateLogger();
+
+ Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
+ }
+ }
+
+ /// <summary>
+ /// Call static initialization methods for the application.
+ /// </summary>
+ public static void PerformStaticInitialization()
+ {
+ // Make sure we have all the code pages we can get
+ // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+ // Increase the max http request limit
+ // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
+ ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
+
+ // Disable the "Expect: 100-Continue" header by default
+ // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
+ ServicePointManager.Expect100Continue = false;
+
+ Batteries_V2.Init();
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 829f294ce..146de3ae1 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -24,31 +24,31 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="CommandLineParser" Version="2.9.1" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.2" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.2" />
- <PackageReference Include="prometheus-net" Version="7.0.0" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
- <PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
- <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
- <PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
- <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
- <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
- <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
- <PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
+ <PackageReference Include="CommandLineParser" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
+ <PackageReference Include="prometheus-net" />
+ <PackageReference Include="prometheus-net.AspNetCore" />
+ <PackageReference Include="Serilog.AspNetCore" />
+ <PackageReference Include="Serilog.Enrichers.Thread" />
+ <PackageReference Include="Serilog.Settings.Configuration" />
+ <PackageReference Include="Serilog.Sinks.Async" />
+ <PackageReference Include="Serilog.Sinks.Console" />
+ <PackageReference Include="Serilog.Sinks.File" />
+ <PackageReference Include="Serilog.Sinks.Graylog" />
+ <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
deleted file mode 100644
index 6ee5bf38a..000000000
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Jellyfin.Networking.Configuration;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Redirect requests without baseurl prefix to the baseurl prefixed URL.
- /// </summary>
- public class BaseUrlRedirectionMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly ILogger<BaseUrlRedirectionMiddleware> _logger;
- private readonly IConfiguration _configuration;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="configuration">The application configuration.</param>
- public BaseUrlRedirectionMiddleware(
- RequestDelegate next,
- ILogger<BaseUrlRedirectionMiddleware> logger,
- IConfiguration configuration)
- {
- _next = next;
- _logger = logger;
- _configuration = configuration;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <param name="serverConfigurationManager">The server configuration manager.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
- {
- var localPath = httpContext.Request.Path.ToString();
- var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
-
- if (string.IsNullOrEmpty(localPath)
- || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase)
- || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
- )
- {
- // Redirect health endpoint
- if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, "/health/", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Redirecting /health check");
- httpContext.Response.Redirect(baseUrlPrefix + "/health");
- return;
- }
-
- // Always redirect back to the default path if the base prefix is invalid or missing
- _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
-
- var port = httpContext.Request.Host.Port ?? -1;
- var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri;
- var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri;
- var target = uri.MakeRelativeUri(redirectUri).ToString();
- _logger.LogDebug("Redirecting to {Target}", target);
-
- httpContext.Response.Redirect(target);
- return;
- }
-
- await _next(httpContext).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
deleted file mode 100644
index 91dbce19a..000000000
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-using System;
-using System.IO;
-using System.Net.Mime;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Exception Middleware.
- /// </summary>
- public class ExceptionMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly ILogger<ExceptionMiddleware> _logger;
- private readonly IServerConfigurationManager _configuration;
- private readonly IWebHostEnvironment _hostEnvironment;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
- /// </summary>
- /// <param name="next">Next request delegate.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param>
- public ExceptionMiddleware(
- RequestDelegate next,
- ILogger<ExceptionMiddleware> logger,
- IServerConfigurationManager serverConfigurationManager,
- IWebHostEnvironment hostEnvironment)
- {
- _next = next;
- _logger = logger;
- _configuration = serverConfigurationManager;
- _hostEnvironment = hostEnvironment;
- }
-
- /// <summary>
- /// Invoke request.
- /// </summary>
- /// <param name="context">Request context.</param>
- /// <returns>Task.</returns>
- public async Task Invoke(HttpContext context)
- {
- try
- {
- await _next(context).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- if (context.Response.HasStarted)
- {
- _logger.LogWarning("The response has already started, the exception middleware will not be executed.");
- throw;
- }
-
- ex = GetActualException(ex);
-
- bool ignoreStackTrace =
- ex is SocketException
- || ex is IOException
- || ex is OperationCanceledException
- || ex is SecurityException
- || ex is AuthenticationException
- || ex is FileNotFoundException;
-
- if (ignoreStackTrace)
- {
- _logger.LogError(
- "Error processing request: {ExceptionMessage}. URL {Method} {Url}.",
- ex.Message.TrimEnd('.'),
- context.Request.Method,
- context.Request.Path);
- }
- else
- {
- _logger.LogError(
- ex,
- "Error processing request. URL {Method} {Url}.",
- context.Request.Method,
- context.Request.Path);
- }
-
- context.Response.StatusCode = GetStatusCode(ex);
- context.Response.ContentType = MediaTypeNames.Text.Plain;
-
- // Don't send exception unless the server is in a Development environment
- var errorContent = _hostEnvironment.IsDevelopment()
- ? NormalizeExceptionMessage(ex.Message)
- : "Error processing request.";
- await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
- }
- }
-
- private static Exception GetActualException(Exception ex)
- {
- if (ex is AggregateException agg)
- {
- var inner = agg.InnerException;
- if (inner is not null)
- {
- return GetActualException(inner);
- }
-
- var inners = agg.InnerExceptions;
- if (inners.Count > 0)
- {
- return GetActualException(inners[0]);
- }
- }
-
- return ex;
- }
-
- private static int GetStatusCode(Exception ex)
- {
- switch (ex)
- {
- case ArgumentException _: return StatusCodes.Status400BadRequest;
- case AuthenticationException _: return StatusCodes.Status401Unauthorized;
- case SecurityException _: return StatusCodes.Status403Forbidden;
- case DirectoryNotFoundException _:
- case FileNotFoundException _:
- case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
- case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
- default: return StatusCodes.Status500InternalServerError;
- }
- }
-
- private string NormalizeExceptionMessage(string msg)
- {
- // Strip any information we don't want to reveal
- return msg.Replace(
- _configuration.ApplicationPaths.ProgramSystemPath,
- string.Empty,
- StringComparison.OrdinalIgnoreCase)
- .Replace(
- _configuration.ApplicationPaths.ProgramDataPath,
- string.Empty,
- StringComparison.OrdinalIgnoreCase);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
deleted file mode 100644
index 0afcd61a0..000000000
--- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System.Net;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Validates the IP of requests coming from local networks wrt. remote access.
- /// </summary>
- public class IpBasedAccessValidationMiddleware
- {
- private readonly RequestDelegate _next;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- public IpBasedAccessValidationMiddleware(RequestDelegate next)
- {
- _next = next;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <param name="networkManager">The network manager.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext, INetworkManager networkManager)
- {
- if (httpContext.IsLocal())
- {
- // Running locally.
- await _next(httpContext).ConfigureAwait(false);
- return;
- }
-
- var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
-
- if (!networkManager.HasRemoteAccess(remoteIp))
- {
- return;
- }
-
- await _next(httpContext).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
deleted file mode 100644
index 67bf24d2a..000000000
--- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Net;
-using System.Threading.Tasks;
-using Jellyfin.Networking.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Validates the LAN host IP based on application configuration.
- /// </summary>
- public class LanFilteringMiddleware
- {
- private readonly RequestDelegate _next;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- public LanFilteringMiddleware(RequestDelegate next)
- {
- _next = next;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <param name="networkManager">The network manager.</param>
- /// <param name="serverConfigurationManager">The server configuration manager.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
- {
- var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
-
- if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
- {
- return;
- }
-
- await _next(httpContext).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
deleted file mode 100644
index b214299df..000000000
--- a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Removes /emby and /mediabrowser from requested route.
- /// </summary>
- public class LegacyEmbyRouteRewriteMiddleware
- {
- private const string EmbyPath = "/emby";
- private const string MediabrowserPath = "/mediabrowser";
-
- private readonly RequestDelegate _next;
- private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- /// <param name="logger">The logger.</param>
- public LegacyEmbyRouteRewriteMiddleware(
- RequestDelegate next,
- ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
- {
- _next = next;
- _logger = logger;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext)
- {
- var localPath = httpContext.Request.Path.ToString();
- if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
- {
- httpContext.Request.Path = localPath[EmbyPath.Length..];
- _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
- }
- else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
- {
- httpContext.Request.Path = localPath[MediabrowserPath.Length..];
- _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
- }
-
- await _next(httpContext).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
deleted file mode 100644
index 24807ce38..000000000
--- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Features;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// URL decodes the querystring before binding.
- /// </summary>
- public class QueryStringDecodingMiddleware
- {
- private readonly RequestDelegate _next;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- public QueryStringDecodingMiddleware(RequestDelegate next)
- {
- _next = next;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext)
- {
- var feature = httpContext.Features.Get<IQueryFeature>();
- if (feature is not null)
- {
- httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature));
- }
-
- await _next(httpContext).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
deleted file mode 100644
index 531897cd4..000000000
--- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System.Diagnostics;
-using System.Globalization;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Response time middleware.
- /// </summary>
- public class ResponseTimeMiddleware
- {
- private const string ResponseHeaderResponseTime = "X-Response-Time-ms";
-
- private readonly RequestDelegate _next;
- private readonly ILogger<ResponseTimeMiddleware> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
- /// </summary>
- /// <param name="next">Next request delegate.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
- public ResponseTimeMiddleware(
- RequestDelegate next,
- ILogger<ResponseTimeMiddleware> logger)
- {
- _next = next;
- _logger = logger;
- }
-
- /// <summary>
- /// Invoke request.
- /// </summary>
- /// <param name="context">Request context.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <returns>Task.</returns>
- public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
- {
- var startTimestamp = Stopwatch.GetTimestamp();
-
- var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
- var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
- context.Response.OnStarting(() =>
- {
- var responseTime = Stopwatch.GetElapsedTime(startTimestamp);
- var responseTimeMs = responseTime.TotalMilliseconds;
- if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug))
- {
- _logger.LogDebug(
- "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
- context.Request.GetDisplayUrl(),
- context.GetNormalizedRemoteIp(),
- responseTime,
- context.Response.StatusCode);
- }
-
- context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture);
- return Task.CompletedTask;
- });
-
- // Call the next delegate/middleware in the pipeline
- await this._next(context).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
deleted file mode 100644
index fabcd2da7..000000000
--- a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Redirect requests to robots.txt to web/robots.txt.
- /// </summary>
- public class RobotsRedirectionMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly ILogger<RobotsRedirectionMiddleware> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- /// <param name="logger">The logger.</param>
- public RobotsRedirectionMiddleware(
- RequestDelegate next,
- ILogger<RobotsRedirectionMiddleware> logger)
- {
- _next = next;
- _logger = logger;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext)
- {
- var localPath = httpContext.Request.Path.ToString();
- if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
- httpContext.Response.Redirect("web/robots.txt");
- return;
- }
-
- await _next(httpContext).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
deleted file mode 100644
index 2ec063392..000000000
--- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Net.Mime;
-using System.Threading.Tasks;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Globalization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Shows a custom message during server startup.
- /// </summary>
- public class ServerStartupMessageMiddleware
- {
- private readonly RequestDelegate _next;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- public ServerStartupMessageMiddleware(RequestDelegate next)
- {
- _next = next;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <param name="serverApplicationHost">The server application host.</param>
- /// <param name="localizationManager">The localization manager.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(
- HttpContext httpContext,
- IServerApplicationHost serverApplicationHost,
- ILocalizationManager localizationManager)
- {
- if (serverApplicationHost.CoreStartupHasCompleted
- || httpContext.Request.Path.Equals("/system/ping", StringComparison.OrdinalIgnoreCase))
- {
- await _next(httpContext).ConfigureAwait(false);
- return;
- }
-
- var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
- httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
- httpContext.Response.ContentType = MediaTypeNames.Text.Html;
- await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
deleted file mode 100644
index 2f1d79157..000000000
--- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Extensions;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Features;
-using Microsoft.Extensions.Primitives;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Defines the <see cref="UrlDecodeQueryFeature"/>.
- /// </summary>
- public class UrlDecodeQueryFeature : IQueryFeature
- {
- private IQueryCollection? _store;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class.
- /// </summary>
- /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param>
- public UrlDecodeQueryFeature(IQueryFeature feature)
- {
- Query = feature.Query;
- }
-
- /// <summary>
- /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>.
- /// </summary>
- public IQueryCollection Query
- {
- get
- {
- return _store ?? QueryCollection.Empty;
- }
-
- set
- {
- // Only interested in where the querystring is encoded which shows up as one key with nothing in the value.
- if (value.Count != 1)
- {
- _store = value;
- return;
- }
-
- // Encoded querystrings have no value, so don't process anything if a value is present.
- var (key, stringValues) = value.First();
- if (!string.IsNullOrEmpty(stringValues))
- {
- _store = value;
- return;
- }
-
- if (!key.Contains('=', StringComparison.Ordinal))
- {
- _store = value;
- return;
- }
-
- var pairs = new Dictionary<string, StringValues>();
- foreach (var pair in key.SpanSplit('&'))
- {
- var i = pair.IndexOf('=');
- if (i == -1)
- {
- // encoded is an equals.
- // We use TryAdd so duplicate keys get ignored
- pairs.TryAdd(pair.ToString(), StringValues.Empty);
- continue;
- }
-
- var k = pair[..i].ToString();
- var v = pair[(i + 1)..].ToString();
- if (!pairs.TryAdd(k, new StringValues(v)))
- {
- pairs[k] = StringValues.Concat(pairs[k], v);
- }
- }
-
- _store = new QueryCollection(pairs);
- }
- }
- }
-}
diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
deleted file mode 100644
index b7a5d2b34..000000000
--- a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Net;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Server.Middleware
-{
- /// <summary>
- /// Handles WebSocket requests.
- /// </summary>
- public class WebSocketHandlerMiddleware
- {
- private readonly RequestDelegate _next;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class.
- /// </summary>
- /// <param name="next">The next delegate in the pipeline.</param>
- public WebSocketHandlerMiddleware(RequestDelegate next)
- {
- _next = next;
- }
-
- /// <summary>
- /// Executes the middleware action.
- /// </summary>
- /// <param name="httpContext">The current HTTP context.</param>
- /// <param name="webSocketManager">The WebSocket connection manager.</param>
- /// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager)
- {
- if (!httpContext.WebSockets.IsWebSocketRequest)
- {
- await _next(httpContext).ConfigureAwait(false);
- return;
- }
-
- await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false);
- }
- }
-}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 23fb9e370..d4bf81f10 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -21,7 +21,9 @@ namespace Jellyfin.Server.Migrations
/// </summary>
private static readonly Type[] _preStartupMigrationTypes =
{
- typeof(PreStartupRoutines.CreateNetworkConfiguration)
+ typeof(PreStartupRoutines.CreateNetworkConfiguration),
+ typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
+ typeof(PreStartupRoutines.MigrateRatingLevels)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
new file mode 100644
index 000000000..14b51bd4c
--- /dev/null
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
@@ -0,0 +1,89 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Serialization;
+using Emby.Server.Implementations;
+using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.PreStartupRoutines;
+
+/// <inheritdoc />
+public class MigrateMusicBrainzTimeout : IMigrationRoutine
+{
+ private readonly ServerApplicationPaths _applicationPaths;
+ private readonly ILogger<MigrateMusicBrainzTimeout> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateMusicBrainzTimeout"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param>
+ /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public MigrateMusicBrainzTimeout(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = loggerFactory.CreateLogger<MigrateMusicBrainzTimeout>();
+ }
+
+ /// <inheritdoc />
+ public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0");
+
+ /// <inheritdoc />
+ public string Name => nameof(MigrateMusicBrainzTimeout);
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ string path = Path.Combine(_applicationPaths.PluginConfigurationsPath, "Jellyfin.Plugin.MusicBrainz.xml");
+ if (!File.Exists(path))
+ {
+ _logger.LogDebug("No MusicBrainz plugin configuration file found, skipping");
+ return;
+ }
+
+ var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
+ using var xmlReader = XmlReader.Create(path);
+ var oldPluginConfiguration = serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
+
+ if (oldPluginConfiguration is not null)
+ {
+ var newPluginConfiguration = new PluginConfiguration();
+ newPluginConfiguration.Server = oldPluginConfiguration.Server;
+ newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName;
+ var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0;
+ newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit;
+
+ var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
+ var xmlWriterSettings = new XmlWriterSettings { Indent = true };
+ using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+ pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
+ }
+ }
+
+#pragma warning disable
+ public sealed class OldMusicBrainzConfiguration
+ {
+ private string _server = string.Empty;
+
+ private long _rateLimit = 0L;
+
+ public string Server
+ {
+ get => _server;
+ set => _server = value.TrimEnd('/');
+ }
+
+ public long RateLimit
+ {
+ get => _rateLimit;
+ set => _rateLimit = value;
+ }
+
+ public bool ReplaceArtistName { get; set; }
+ }
+#pragma warning restore
+
+}
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs
new file mode 100644
index 000000000..465bbd7fe
--- /dev/null
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Globalization;
+using System.IO;
+
+using Emby.Server.Implementations;
+using MediaBrowser.Controller;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Jellyfin.Server.Migrations.PreStartupRoutines
+{
+ /// <summary>
+ /// Migrate rating levels to new rating level system.
+ /// </summary>
+ internal class MigrateRatingLevels : IMigrationRoutine
+ {
+ private const string DbFilename = "library.db";
+ private readonly ILogger<MigrateRatingLevels> _logger;
+ private readonly IServerApplicationPaths _applicationPaths;
+
+ public MigrateRatingLevels(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
+
+ /// <inheritdoc/>
+ public string Name => "MigrateRatingLevels";
+
+ /// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var dataPath = _applicationPaths.DataPath;
+ var dbPath = Path.Combine(dataPath, DbFilename);
+ using (var connection = SQLite3.Open(
+ dbPath,
+ ConnectionFlags.ReadWrite,
+ null))
+ {
+ // Back up the database before deleting any entries
+ for (int i = 1; ; i++)
+ {
+ var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
+ if (!File.Exists(bakPath))
+ {
+ try
+ {
+ File.Copy(dbPath, bakPath);
+ _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
+ throw;
+ }
+ }
+ }
+
+ // Migrate parental rating levels to new schema
+ _logger.LogInformation("Migrating parental rating levels.");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating = 'NR'");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = ''");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = 0");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 100");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 15");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 10");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 9");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 16 WHERE InheritedParentalRatingValue = 8");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 7");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 6");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 5");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 7 WHERE InheritedParentalRatingValue = 4");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 3");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 2");
+ connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 0 WHERE InheritedParentalRatingValue = 1");
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index bf66f75ff..e8a0af9f8 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines
private const string DbFilename = "activitylog.db";
private readonly ILogger<MigrateActivityLogDb> _logger;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
private readonly IServerApplicationPaths _paths;
/// <summary>
@@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines
/// <param name="logger">The logger.</param>
/// <param name="paths">The server application paths.</param>
/// <param name="provider">The database provider.</param>
- public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDb> provider)
+ public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDbContext> provider)
{
_logger = logger;
_provider = provider;
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index bf1ea8233..09daae0ff 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines
private const string DbFilename = "authentication.db";
private readonly ILogger<MigrateAuthenticationDb> _logger;
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IServerApplicationPaths _appPaths;
private readonly IUserManager _userManager;
@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines
/// <param name="userManager">The user manager.</param>
public MigrateAuthenticationDb(
ILogger<MigrateAuthenticationDb> logger,
- IDbContextFactory<JellyfinDb> dbProvider,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
IServerApplicationPaths appPaths,
IUserManager userManager)
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 0fad77cfe..7c4ffdbc0 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -25,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
private readonly JsonSerializerOptions _jsonOptions;
private readonly IUserManager _userManager;
@@ -39,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines
public MigrateDisplayPreferencesDb(
ILogger<MigrateDisplayPreferencesDb> logger,
IServerApplicationPaths paths,
- IDbContextFactory<JellyfinDb> provider,
+ IDbContextFactory<JellyfinDbContext> provider,
IUserManager userManager)
{
_logger = logger;
@@ -130,7 +130,7 @@ namespace Jellyfin.Server.Migrations.Routines
SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength)
? skipForwardLength
: 30000,
- SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength)
+ SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(length, out var skipBackwardLength)
? skipBackwardLength
: 10000,
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled)
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 2dbd82e8f..9bf1e6b80 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateUserDb> _logger;
private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
private readonly IXmlSerializer _xmlSerializer;
/// <summary>
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
public MigrateUserDb(
ILogger<MigrateUserDb> logger,
IServerApplicationPaths paths,
- IDbContextFactory<JellyfinDb> provider,
+ IDbContextFactory<JellyfinDbContext> provider,
IXmlSerializer xmlSerializer)
{
_logger = logger;
@@ -163,6 +163,7 @@ namespace Jellyfin.Server.Migrations.Routines
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+ user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
foreach (var policyAccessSchedule in policy.AccessSchedules)
{
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 540375dce..6e8b17a73 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -1,22 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.Linq;
-using System.Net;
using System.Reflection;
-using System.Runtime.Versioning;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
+using Jellyfin.Server.Extensions;
+using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.IO;
-using Microsoft.AspNetCore.Hosting;
+using MediaBrowser.Controller;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -25,7 +21,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
-using SQLitePCL;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -46,8 +41,9 @@ namespace Jellyfin.Server
/// </summary>
public const string LoggingConfigFileSystem = "logging.json";
- private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
+ private static CancellationTokenSource _tokenSource = new();
+ private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@@ -92,14 +88,14 @@ namespace Jellyfin.Server
private static async Task StartApp(StartupOptions options)
{
- var startTimestamp = Stopwatch.GetTimestamp();
+ _startTimestamp = Stopwatch.GetTimestamp();
// Log all uncaught exceptions to std error
static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
- Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString());
+ Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject);
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
- ServerApplicationPaths appPaths = CreateApplicationPaths(options);
+ ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
@@ -108,13 +104,12 @@ namespace Jellyfin.Server
Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1");
Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1");
- await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
+ await StartupHelpers.InitLoggingConfigFile(appPaths).ConfigureAwait(false);
// Create an instance of the application configuration to use for application startup
IConfiguration startupConfig = CreateAppConfiguration(options, appPaths);
- // Initialize logging framework
- InitializeLoggingFramework(startupConfig, appPaths);
+ StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
// Log uncaught exceptions to the logging instead of std error
@@ -153,19 +148,19 @@ namespace Jellyfin.Server
"Jellyfin version: {Version}",
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
- ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
+ StartupHelpers.LogEnvironmentInfo(_logger, appPaths);
// If hosting the web client, validate the client content path
if (startupConfig.HostWebClient())
{
- string? webContentPath = appPaths.WebPath;
+ var webContentPath = appPaths.WebPath;
if (!Directory.Exists(webContentPath) || !Directory.EnumerateFiles(webContentPath).Any())
{
_logger.LogError(
"The server is expected to host the web client, but the provided content directory is either " +
"invalid or empty: {WebContentPath}. If you do not want to host the web client with the " +
"server, you may set the '--nowebclient' command line flag, or set" +
- "'{ConfigKey}=false' in your config settings.",
+ "'{ConfigKey}=false' in your config settings",
webContentPath,
HostWebClientKey);
Environment.ExitCode = 1;
@@ -173,20 +168,36 @@ namespace Jellyfin.Server
}
}
- PerformStaticInitialization();
+ StartupHelpers.PerformStaticInitialization();
Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory);
+ do
+ {
+ _restartOnShutdown = false;
+ await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
+
+ if (_restartOnShutdown)
+ {
+ _tokenSource = new CancellationTokenSource();
+ _startTimestamp = Stopwatch.GetTimestamp();
+ }
+ } while (_restartOnShutdown);
+ }
+
+ private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
+ {
var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
startupConfig);
+ IHost? host = null;
try
{
- var host = Host.CreateDefaultBuilder()
+ host = Host.CreateDefaultBuilder()
.ConfigureServices(services => appHost.Init(services))
- .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths))
+ .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
.UseSerilog()
.Build();
@@ -203,20 +214,20 @@ namespace Jellyfin.Server
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
- var socketPath = GetUnixSocketPath(startupConfig, appPaths);
+ var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths);
- SetUnixSocketPermissions(startupConfig, socketPath);
+ StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
}
}
catch (Exception ex) when (ex is not TaskCanceledException)
{
- _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
+ _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again");
throw;
}
await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
- _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(startTimestamp));
+ _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
// Block main thread until shutdown
await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
@@ -227,7 +238,7 @@ namespace Jellyfin.Server
}
catch (Exception ex)
{
- _logger.LogCritical(ex, "Error while starting server.");
+ _logger.LogCritical(ex, "Error while starting server");
}
finally
{
@@ -236,7 +247,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
- var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
+ var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
if (context.Database.IsSqlite())
@@ -247,300 +258,7 @@ namespace Jellyfin.Server
}
await appHost.DisposeAsync().ConfigureAwait(false);
- }
-
- if (_restartOnShutdown)
- {
- StartNewInstance(options);
- }
- }
-
- /// <summary>
- /// Call static initialization methods for the application.
- /// </summary>
- public static void PerformStaticInitialization()
- {
- // Make sure we have all the code pages we can get
- // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
- Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-
- // Increase the max http request limit
- // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
- ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
-
- // Disable the "Expect: 100-Continue" header by default
- // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
- ServicePointManager.Expect100Continue = false;
-
- Batteries_V2.Init();
- }
-
- /// <summary>
- /// Configure the web host builder.
- /// </summary>
- /// <param name="builder">The builder to configure.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="startupConfig">The application configuration.</param>
- /// <param name="appPaths">The application paths.</param>
- /// <returns>The configured web host builder.</returns>
- public static IWebHostBuilder ConfigureWebHostBuilder(
- this IWebHostBuilder builder,
- CoreAppHost appHost,
- IConfiguration startupConfig,
- IApplicationPaths appPaths)
- {
- return builder
- .UseKestrel((builderContext, options) =>
- {
- var addresses = appHost.NetManager.GetAllBindInterfaces();
-
- bool flagged = false;
- foreach (IPObject netAdd in addresses)
- {
- _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd);
- options.Listen(netAdd.Address, appHost.HttpPort);
- if (appHost.ListenWithHttps)
- {
- options.Listen(
- netAdd.Address,
- appHost.HttpsPort,
- listenOptions => listenOptions.UseHttps(appHost.Certificate));
- }
- else if (builderContext.HostingEnvironment.IsDevelopment())
- {
- try
- {
- options.Listen(
- netAdd.Address,
- appHost.HttpsPort,
- listenOptions => listenOptions.UseHttps());
- }
- catch (InvalidOperationException)
- {
- if (!flagged)
- {
- _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
- flagged = true;
- }
- }
- }
- }
-
- // Bind to unix socket (only on unix systems)
- if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix)
- {
- var socketPath = GetUnixSocketPath(startupConfig, appPaths);
-
- // Workaround for https://github.com/aspnet/AspNetCore/issues/14134
- if (File.Exists(socketPath))
- {
- File.Delete(socketPath);
- }
-
- options.ListenUnixSocket(socketPath);
- _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
- }
- })
- .UseStartup(_ => new Startup(appHost));
- }
-
- /// <summary>
- /// Create the data, config and log paths from the variety of inputs(command line args,
- /// environment variables) or decide on what default to use. For Windows it's %AppPath%
- /// for everything else the
- /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
- /// is followed.
- /// </summary>
- /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
- /// <returns><see cref="ServerApplicationPaths" />.</returns>
- private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
- {
- // dataDir
- // IF --datadir
- // ELSE IF $JELLYFIN_DATA_DIR
- // ELSE IF windows, use <%APPDATA%>/jellyfin
- // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
- // ELSE use $HOME/.local/share/jellyfin
- var dataDir = options.DataDir;
- if (string.IsNullOrEmpty(dataDir))
- {
- dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR");
-
- if (string.IsNullOrEmpty(dataDir))
- {
- // LocalApplicationData follows the XDG spec on unix machines
- dataDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "jellyfin");
- }
- }
-
- // configDir
- // IF --configdir
- // ELSE IF $JELLYFIN_CONFIG_DIR
- // ELSE IF --datadir, use <datadir>/config (assume portable run)
- // ELSE IF <datadir>/config exists, use that
- // ELSE IF windows, use <datadir>/config
- // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin
- // ELSE $HOME/.config/jellyfin
- var configDir = options.ConfigDir;
- if (string.IsNullOrEmpty(configDir))
- {
- configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
-
- if (string.IsNullOrEmpty(configDir))
- {
- if (options.DataDir is not null
- || Directory.Exists(Path.Combine(dataDir, "config"))
- || OperatingSystem.IsWindows())
- {
- // Hang config folder off already set dataDir
- configDir = Path.Combine(dataDir, "config");
- }
- else
- {
- // $XDG_CONFIG_HOME defines the base directory relative to which
- // user specific configuration files should be stored.
- configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
-
- // If $XDG_CONFIG_HOME is either not set or empty,
- // a default equal to $HOME /.config should be used.
- if (string.IsNullOrEmpty(configDir))
- {
- configDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".config");
- }
-
- configDir = Path.Combine(configDir, "jellyfin");
- }
- }
- }
-
- // cacheDir
- // IF --cachedir
- // ELSE IF $JELLYFIN_CACHE_DIR
- // ELSE IF windows, use <datadir>/cache
- // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin
- // ELSE HOME/.cache/jellyfin
- var cacheDir = options.CacheDir;
- if (string.IsNullOrEmpty(cacheDir))
- {
- cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
-
- if (string.IsNullOrEmpty(cacheDir))
- {
- if (OperatingSystem.IsWindows())
- {
- // Hang cache folder off already set dataDir
- cacheDir = Path.Combine(dataDir, "cache");
- }
- else
- {
- // $XDG_CACHE_HOME defines the base directory relative to which
- // user specific non-essential data files should be stored.
- cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
-
- // If $XDG_CACHE_HOME is either not set or empty,
- // a default equal to $HOME/.cache should be used.
- if (string.IsNullOrEmpty(cacheDir))
- {
- cacheDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".cache");
- }
-
- cacheDir = Path.Combine(cacheDir, "jellyfin");
- }
- }
- }
-
- // webDir
- // IF --webdir
- // ELSE IF $JELLYFIN_WEB_DIR
- // ELSE <bindir>/jellyfin-web
- var webDir = options.WebDir;
- if (string.IsNullOrEmpty(webDir))
- {
- webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
-
- if (string.IsNullOrEmpty(webDir))
- {
- // Use default location under ResourcesPath
- webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web");
- }
- }
-
- // logDir
- // IF --logdir
- // ELSE IF $JELLYFIN_LOG_DIR
- // ELSE IF --datadir, use <datadir>/log (assume portable run)
- // ELSE <datadir>/log
- var logDir = options.LogDir;
- if (string.IsNullOrEmpty(logDir))
- {
- logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
-
- if (string.IsNullOrEmpty(logDir))
- {
- // Hang log folder off already set dataDir
- logDir = Path.Combine(dataDir, "log");
- }
- }
-
- // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
- dataDir = Path.GetFullPath(dataDir);
- logDir = Path.GetFullPath(logDir);
- configDir = Path.GetFullPath(configDir);
- cacheDir = Path.GetFullPath(cacheDir);
- webDir = Path.GetFullPath(webDir);
-
- // Ensure the main folders exist before we continue
- try
- {
- Directory.CreateDirectory(dataDir);
- Directory.CreateDirectory(logDir);
- Directory.CreateDirectory(configDir);
- Directory.CreateDirectory(cacheDir);
- }
- catch (IOException ex)
- {
- Console.Error.WriteLine("Error whilst attempting to create folder");
- Console.Error.WriteLine(ex.ToString());
- Environment.Exit(1);
- }
-
- return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
- }
-
- /// <summary>
- /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
- /// already.
- /// </summary>
- /// <param name="appPaths">The application paths.</param>
- /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
- public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
- {
- // Do nothing if the config file already exists
- string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault);
- if (File.Exists(configPath))
- {
- return;
- }
-
- // Get a stream of the resource contents
- // NOTE: The .csproj name is used instead of the assembly name in the resource path
- const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
- Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
- ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
- await using (resource.ConfigureAwait(false))
- {
- Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await using (dst.ConfigureAwait(false))
- {
- // Copy the resource contents to the expected file path for the config file
- await resource.CopyToAsync(dst).ConfigureAwait(false);
- }
+ host?.Dispose();
}
}
@@ -578,112 +296,5 @@ namespace Jellyfin.Server
.AddEnvironmentVariables("JELLYFIN_")
.AddInMemoryCollection(commandLineOpts.ConvertToConfig());
}
-
- /// <summary>
- /// Initialize Serilog using configuration and fall back to defaults on failure.
- /// </summary>
- private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
- {
- try
- {
- // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
- Log.Logger = new LoggerConfiguration()
- .ReadFrom.Configuration(configuration)
- .Enrich.FromLogContext()
- .Enrich.WithThreadId()
- .CreateLogger();
- }
- catch (Exception ex)
- {
- Log.Logger = new LoggerConfiguration()
- .WriteTo.Console(
- outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
- formatProvider: CultureInfo.InvariantCulture)
- .WriteTo.Async(x => x.File(
- Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
- rollingInterval: RollingInterval.Day,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
- formatProvider: CultureInfo.InvariantCulture,
- encoding: Encoding.UTF8))
- .Enrich.FromLogContext()
- .Enrich.WithThreadId()
- .CreateLogger();
-
- Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
- }
- }
-
- private static void StartNewInstance(StartupOptions options)
- {
- _logger.LogInformation("Starting new instance");
-
- var module = options.RestartPath;
-
- if (string.IsNullOrWhiteSpace(module))
- {
- module = Environment.GetCommandLineArgs()[0];
- }
-
- string commandLineArgsString;
- if (options.RestartArgs is not null)
- {
- commandLineArgsString = options.RestartArgs;
- }
- else
- {
- commandLineArgsString = string.Join(
- ' ',
- Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument));
- }
-
- _logger.LogInformation("Executable: {0}", module);
- _logger.LogInformation("Arguments: {0}", commandLineArgsString);
-
- Process.Start(module, commandLineArgsString);
- }
-
- private static string NormalizeCommandLineArgument(string arg)
- {
- if (!arg.Contains(' ', StringComparison.Ordinal))
- {
- return arg;
- }
-
- return "\"" + arg + "\"";
- }
-
- private static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths)
- {
- var socketPath = startupConfig.GetUnixSocketPath();
-
- if (string.IsNullOrEmpty(socketPath))
- {
- var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
- var socketFile = "jellyfin.sock";
- if (xdgRuntimeDir is null)
- {
- // Fall back to config dir
- socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile);
- }
- else
- {
- socketPath = Path.Join(xdgRuntimeDir, socketFile);
- }
- }
-
- return socketPath;
- }
-
- [UnsupportedOSPlatform("windows")]
- private static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath)
- {
- var socketPerms = startupConfig.GetUnixSocketPermissions();
-
- if (!string.IsNullOrEmpty(socketPerms))
- {
- File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8));
- _logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms);
- }
- }
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index f89f81c76..155f9fc8c 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -4,7 +4,9 @@ using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
+using System.Runtime.InteropServices;
using System.Text;
+using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
@@ -12,7 +14,6 @@ using Jellyfin.Server.HealthChecks;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Infrastructure;
-using Jellyfin.Server.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -108,7 +109,7 @@ namespace Jellyfin.Server
string.Format(
CultureInfo.InvariantCulture,
"{0}/{1} UPnP/1.0 {2}/{3}",
- MediaBrowser.Common.System.OperatingSystem.Name,
+ Environment.OSVersion.Platform,
Environment.OSVersion,
_serverApplicationHost.Name,
_serverApplicationHost.ApplicationVersionString));
@@ -119,7 +120,7 @@ namespace Jellyfin.Server
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
services.AddHealthChecks()
- .AddCheck<DbContextFactoryHealthCheck<JellyfinDb>>(nameof(JellyfinDb));
+ .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator();
}
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index 0d9f379e0..c3989751c 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -64,14 +64,6 @@ namespace Jellyfin.Server
public string? PackageName { get; set; }
/// <inheritdoc />
- [Option("restartpath", Required = false, HelpText = "Path to restart script.")]
- public string? RestartPath { get; set; }
-
- /// <inheritdoc />
- [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
- public string? RestartArgs { get; set; }
-
- /// <inheritdoc />
[Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")]
public string? PublishedServerUrl { get; set; }
diff --git a/Jellyfin.sln b/Jellyfin.sln
index c0d2ec068..cad23fc5e 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -21,16 +21,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing", "src\Jel
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby.Notifications\Emby.Notifications.csproj", "{2E030C33-6923-4530-9E54-FA29FA6AD1A9}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}"
@@ -139,10 +135,6 @@ Global
{89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU
- {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -155,10 +147,6 @@ Global
{805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU
- {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.Build.0 = Release|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/Jellyfin.sln.DotSettings b/Jellyfin.sln.DotSettings
new file mode 100644
index 000000000..b56741648
--- /dev/null
+++ b/Jellyfin.sln.DotSettings
@@ -0,0 +1,3 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=Jellyfin/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=Playstate/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 53683cdbd..96ee701b3 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -48,12 +48,6 @@ namespace MediaBrowser.Common
bool IsShuttingDown { get; }
/// <summary>
- /// Gets a value indicating whether this instance can self restart.
- /// </summary>
- /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
- bool CanSelfRestart { get; }
-
- /// <summary>
/// Gets the application version.
/// </summary>
/// <value>The application version.</value>
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 0296974b5..3f1a098e4 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -19,9 +19,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@@ -49,13 +49,13 @@
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Common/Net/CustomHeaderNames.cs b/MediaBrowser.Common/Net/CustomHeaderNames.cs
deleted file mode 100644
index 5ca9897eb..000000000
--- a/MediaBrowser.Common/Net/CustomHeaderNames.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Common.Net
-{
- public static class CustomHeaderNames
- {
- // Other Headers
- public const string XForwardedFor = "X-Forwarded-For";
- public const string XForwardedPort = "X-Forwarded-Port";
- public const string XForwardedProto = "X-Forwarded-Proto";
- public const string XRealIP = "X-Real-IP";
- }
-}
diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs
index 7cf1b8aa0..ec76a43b6 100644
--- a/MediaBrowser.Common/Net/IPHost.cs
+++ b/MediaBrowser.Common/Net/IPHost.cs
@@ -190,7 +190,7 @@ namespace MediaBrowser.Common.Net
/// <returns>Object representing the string, if it has successfully been parsed.</returns>
public static IPHost Parse(string host)
{
- if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res))
+ if (IPHost.TryParse(host, out IPHost res))
{
return res;
}
@@ -206,7 +206,7 @@ namespace MediaBrowser.Common.Net
/// <returns>Object representing the string, if it has successfully been parsed.</returns>
public static IPHost Parse(string host, AddressFamily family)
{
- if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res))
+ if (IPHost.TryParse(host, out IPHost res))
{
if (family == AddressFamily.InterNetwork)
{
diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs
index ac3396a9f..de72d978e 100644
--- a/MediaBrowser.Common/Net/IPNetAddress.cs
+++ b/MediaBrowser.Common/Net/IPNetAddress.cs
@@ -167,6 +167,11 @@ namespace MediaBrowser.Common.Net
address = address.MapToIPv4();
}
+ if (address.AddressFamily != AddressFamily)
+ {
+ return false;
+ }
+
var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength);
return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix;
}
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
index 176bcbbd5..fa92d383a 100644
--- a/MediaBrowser.Common/Plugins/IPluginManager.cs
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -30,6 +30,11 @@ namespace MediaBrowser.Common.Plugins
IEnumerable<Assembly> LoadAssemblies();
/// <summary>
+ /// Unloads all of the assemblies.
+ /// </summary>
+ void UnloadAssemblies();
+
+ /// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
diff --git a/MediaBrowser.Common/System/OperatingSystem.cs b/MediaBrowser.Common/System/OperatingSystem.cs
deleted file mode 100644
index 5f673d320..000000000
--- a/MediaBrowser.Common/System/OperatingSystem.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Runtime.InteropServices;
-using System.Threading;
-using MediaBrowser.Model.System;
-
-namespace MediaBrowser.Common.System
-{
- public static class OperatingSystem
- {
- // We can't use Interlocked.CompareExchange for enums
- private static int _id = int.MaxValue;
-
- public static OperatingSystemId Id
- {
- get
- {
- if (_id == int.MaxValue)
- {
- Interlocked.CompareExchange(ref _id, (int)GetId(), int.MaxValue);
- }
-
- return (OperatingSystemId)_id;
- }
- }
-
- public static string Name
- {
- get
- {
- switch (Id)
- {
- case OperatingSystemId.BSD: return "BSD";
- case OperatingSystemId.Linux: return "Linux";
- case OperatingSystemId.Darwin: return "macOS";
- case OperatingSystemId.Windows: return "Windows";
- default: throw new PlatformNotSupportedException($"Unknown OS {Id}");
- }
- }
- }
-
- private static OperatingSystemId GetId()
- {
- switch (Environment.OSVersion.Platform)
- {
- // On .NET Core `MacOSX` got replaced by `Unix`, this case should never be hit.
- case PlatformID.MacOSX:
- return OperatingSystemId.Darwin;
- case PlatformID.Win32NT:
- return OperatingSystemId.Windows;
- case PlatformID.Unix:
- default:
- {
- string osDescription = RuntimeInformation.OSDescription;
- if (osDescription.Contains("linux", StringComparison.OrdinalIgnoreCase))
- {
- return OperatingSystemId.Linux;
- }
- else if (osDescription.Contains("darwin", StringComparison.OrdinalIgnoreCase))
- {
- return OperatingSystemId.Darwin;
- }
- else if (osDescription.Contains("bsd", StringComparison.OrdinalIgnoreCase))
- {
- return OperatingSystemId.BSD;
- }
-
- throw new PlatformNotSupportedException($"Can't resolve OS with description: '{osDescription}'");
- }
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
index 49be897ef..8eb27888a 100644
--- a/MediaBrowser.Controller/Channels/IChannelManager.cs
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Channels
public interface IChannelManager
{
/// <summary>
- /// Adds the parts.
- /// </summary>
- /// <param name="channels">The channels.</param>
- void AddParts(IEnumerable<IChannel> channels);
-
- /// <summary>
/// Gets the channel features.
/// </summary>
/// <param name="id">The identifier.</param>
@@ -52,14 +46,14 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
+ Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query);
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
+ Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query);
/// <summary>
/// Gets the latest channel items.
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
index 11e663301..7912c5e87 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -42,8 +42,6 @@ namespace MediaBrowser.Controller.Drawing
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
- public bool AddPlayedIndicator { get; set; }
-
public int? UnplayedCount { get; set; }
public int? Blur { get; set; }
@@ -111,7 +109,6 @@ namespace MediaBrowser.Controller.Drawing
{
return (Quality >= 90) &&
IsFormatSupported(originalImagePath) &&
- !AddPlayedIndicator &&
PercentPlayed.Equals(0) &&
!UnplayedCount.HasValue &&
!Blur.HasValue &&
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index 89aafc84f..22453f0f7 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CA1002
using System.Collections.Generic;
@@ -28,7 +27,7 @@ namespace MediaBrowser.Controller.Dto
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
/// <returns>BaseItemDto.</returns>
- BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null);
+ BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null);
/// <summary>
/// Gets the base item dtos.
@@ -38,7 +37,7 @@ namespace MediaBrowser.Controller.Dto
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
/// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns>
- IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
+ IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null);
/// <summary>
/// Gets the item by name dto.
@@ -48,6 +47,6 @@ namespace MediaBrowser.Controller.Dto
/// <param name="taggedItems">The list of tagged items.</param>
/// <param name="user">The user.</param>
/// <returns>The item dto.</returns>
- BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
+ BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null);
}
}
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 08c622cde..d789033f1 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities
var path = ContainingFolderPath;
- var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path)
};
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index f2c2007f7..a04f02bf9 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
- = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
+ = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
{
@@ -554,7 +554,7 @@ namespace MediaBrowser.Controller.Entities
public string OfficialRating { get; set; }
[JsonIgnore]
- public int InheritedParentalRatingValue { get; set; }
+ public int? InheritedParentalRatingValue { get; set; }
/// <summary>
/// Gets or sets the critic rating.
@@ -893,16 +893,6 @@ namespace MediaBrowser.Controller.Entities
var sortable = Name.Trim().ToLowerInvariant();
- foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
- {
- sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
- }
-
- foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
- {
- sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
- }
-
foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
{
// Remove from beginning if a space follows
@@ -921,12 +911,22 @@ namespace MediaBrowser.Controller.Entities
}
}
+ foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+ {
+ sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
+ }
+
+ foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+ {
+ sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
+ }
+
return ModifySortChunks(sortable);
}
- internal static string ModifySortChunks(string name)
+ internal static string ModifySortChunks(ReadOnlySpan<char> name)
{
- void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
+ static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
{
if (isDigitChunk && chunk.Length < 10)
{
@@ -936,7 +936,7 @@ namespace MediaBrowser.Controller.Entities
builder.Append(chunk);
}
- if (name.Length == 0)
+ if (name.IsEmpty)
{
return string.Empty;
}
@@ -950,13 +950,13 @@ namespace MediaBrowser.Controller.Entities
var isDigit = char.IsDigit(name[i]);
if (isDigit != isDigitChunk)
{
- AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart, i - chunkStart));
+ AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
chunkStart = i;
isDigitChunk = isDigit;
}
}
- AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart));
+ AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
return builder.ToString().RemoveDiacritics();
@@ -1534,12 +1534,6 @@ namespace MediaBrowser.Controller.Entities
}
var maxAllowedRating = user.MaxParentalAgeRating;
-
- if (maxAllowedRating is null)
- {
- return true;
- }
-
var rating = CustomRatingForComparison;
if (string.IsNullOrEmpty(rating))
@@ -1549,12 +1543,13 @@ namespace MediaBrowser.Controller.Entities
if (string.IsNullOrEmpty(rating))
{
+ Logger.LogDebug("{0} has no parental rating set.", Name);
return !GetBlockUnratedValue(user);
}
var value = LocalizationManager.GetRatingLevel(rating);
- // Could not determine the integer value
+ // Could not determine rating level
if (!value.HasValue)
{
var isAllowed = !GetBlockUnratedValue(user);
@@ -1567,7 +1562,7 @@ namespace MediaBrowser.Controller.Entities
return isAllowed;
}
- return value.Value <= maxAllowedRating.Value;
+ return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value;
}
public int? GetInheritedParentalRatingValue()
@@ -1607,6 +1602,12 @@ namespace MediaBrowser.Controller.Entities
return false;
}
+ var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
+ if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
return true;
}
@@ -1621,10 +1622,10 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Gets the block unrated value.
+ /// Gets a bool indicating if access to the unrated item is blocked or not.
/// </summary>
/// <param name="user">The configuration.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
protected virtual bool GetBlockUnratedValue(User user)
{
// Don't block plain folders that are unrated. Let the media underneath get blocked
@@ -2511,7 +2512,7 @@ namespace MediaBrowser.Controller.Entities
var item = this;
- var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0;
+ var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null;
if (inheritedParentalRatingValue != item.InheritedParentalRatingValue)
{
item.InheritedParentalRatingValue = inheritedParentalRatingValue;
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 5ac619d8f..095b261c0 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Entities
{
var path = ContainingFolderPath;
- var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path),
Parent = GetParent() as Folder,
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index a1e531904..a51299284 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Entities
EnableTotalRecordCount = true;
ExcludeArtistIds = Array.Empty<Guid>();
ExcludeInheritedTags = Array.Empty<string>();
+ IncludeInheritedTags = Array.Empty<string>();
ExcludeItemIds = Array.Empty<Guid>();
ExcludeItemTypes = Array.Empty<BaseItemKind>();
ExcludeTags = Array.Empty<string>();
@@ -95,6 +96,8 @@ namespace MediaBrowser.Controller.Entities
public string[] ExcludeInheritedTags { get; set; }
+ public string[] IncludeInheritedTags { get; set; }
+
public IReadOnlyList<string> Genres { get; set; }
public bool? IsSpecialSeason { get; set; }
@@ -368,6 +371,7 @@ namespace MediaBrowser.Controller.Entities
}
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
+ IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
User = user;
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 882abc927..66210cb6c 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -104,7 +104,7 @@ namespace MediaBrowser.Controller.Entities.Movies
public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
{
- return true;
+ return user.HasPermission(PermissionKind.IsAdministrator) || user.HasPermission(PermissionKind.EnableCollectionManagement);
}
public override bool IsSaveLocalMetadataEnabled()
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index c83149a6d..597b4cecb 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -308,6 +308,11 @@ namespace MediaBrowser.Controller.Entities.TV
id.SeriesDisplayOrder = series.DisplayOrder;
}
+ if (Season is not null)
+ {
+ id.SeasonProviderIds = Season.ProviderIds;
+ }
+
id.IsMissingEpisode = IsMissingEpisode;
id.IndexNumberEnd = IndexNumberEnd;
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 993e3e18f..37b4afcf3 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -47,14 +45,14 @@ namespace MediaBrowser.Controller.Library
/// <param name="id">The id.</param>
/// <returns>The user with the specified Id, or <c>null</c> if the user doesn't exist.</returns>
/// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception>
- User GetUserById(Guid id);
+ User? GetUserById(Guid id);
/// <summary>
/// Gets the name of the user by.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>User.</returns>
- User GetUserByName(string name);
+ User? GetUserByName(string name);
/// <summary>
/// Renames the user.
@@ -128,7 +126,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <returns>UserDto.</returns>
- UserDto GetUserDto(User user, string remoteEndPoint = null);
+ UserDto GetUserDto(User user, string? remoteEndPoint = null);
/// <summary>
/// Authenticates the user.
@@ -139,7 +137,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="remoteEndPoint">Remove endpoint to use.</param>
/// <param name="isUserSession">Specifies if a user session.</param>
/// <returns>User wrapped in awaitable task.</returns>
- Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
+ Task<User?> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
/// <summary>
/// Starts the forgot password process.
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 01986d303..c70102167 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -1,12 +1,11 @@
#nullable disable
-#pragma warning disable CA1721, CA1819, CS1591
+#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
@@ -23,22 +22,20 @@ namespace MediaBrowser.Controller.Library
/// </summary>
private readonly IServerApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
private LibraryOptions _libraryOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
/// </summary>
/// <param name="appPaths">The app paths.</param>
- /// <param name="directoryService">The directory service.</param>
- public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService)
+ /// <param name="libraryManager">The library manager.</param>
+ public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager)
{
_appPaths = appPaths;
- DirectoryService = directoryService;
+ _libraryManager = libraryManager;
}
- // TODO remove dependencies as properties, they should be injected where it makes sense
- public IDirectoryService DirectoryService { get; }
-
/// <summary>
/// Gets or sets the file system children.
/// </summary>
@@ -47,7 +44,7 @@ namespace MediaBrowser.Controller.Library
public LibraryOptions LibraryOptions
{
- get => _libraryOptions ??= Parent is null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent);
+ get => _libraryOptions ??= Parent is null ? new LibraryOptions() : _libraryManager.GetLibraryOptions(Parent);
set => _libraryOptions = value;
}
@@ -231,21 +228,15 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the configured content type for the path.
/// </summary>
- /// <remarks>
- /// This is subject to future refactoring as it relies on a static property in BaseItem.
- /// </remarks>
/// <returns>The configured content type.</returns>
public string GetConfiguredContentType()
{
- return BaseItem.LibraryManager.GetConfiguredContentType(Path);
+ return _libraryManager.GetConfiguredContentType(Path);
}
/// <summary>
/// Gets the file system children that do not hit the ignore file check.
/// </summary>
- /// <remarks>
- /// This is subject to future refactoring as it relies on a static property in BaseItem.
- /// </remarks>
/// <returns>The file system children that are not ignored.</returns>
public IEnumerable<FileSystemMetadata> GetActualFileSystemChildren()
{
@@ -253,7 +244,7 @@ namespace MediaBrowser.Controller.Library
for (var i = 0; i < numberOfChildren; i++)
{
var child = FileSystemChildren[i];
- if (BaseItem.LibraryManager.IgnoreFile(child, Parent))
+ if (_libraryManager.IgnoreFile(child, Parent))
{
continue;
}
diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
index 7bc8fa5ab..6d2c3c3d2 100644
--- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
+++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -9,7 +7,7 @@ namespace MediaBrowser.Controller.Library
{
public static class LibraryManagerExtensions
{
- public static BaseItem GetItemById(this ILibraryManager manager, string id)
+ public static BaseItem? GetItemById(this ILibraryManager manager, string id)
{
return manager.GetItemById(new Guid(id));
}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 46bdca302..3b6a16dee 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <returns>A recording.</returns>
- QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
+ Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options);
/// <summary>
/// Gets the timers.
@@ -308,6 +308,6 @@ namespace MediaBrowser.Controller.LiveTv
void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
- List<BaseItem> GetRecordingFolders(User user);
+ Task<BaseItem[]> GetRecordingFoldersAsync(User user);
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index 978826042..f11e3c8f6 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -105,12 +106,9 @@ namespace MediaBrowser.Controller.LiveTv
protected override string CreateSortName()
{
- if (!string.IsNullOrEmpty(Number))
+ if (double.TryParse(Number, CultureInfo.InvariantCulture, out double number))
{
- if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out double number))
- {
- return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty);
- }
+ return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty);
}
return (Number ?? string.Empty) + "-" + (Name ?? string.Empty);
@@ -122,9 +120,7 @@ namespace MediaBrowser.Controller.LiveTv
}
public IEnumerable<BaseItem> GetTaggedItems()
- {
- return new List<BaseItem>();
- }
+ => Enumerable.Empty<BaseItem>();
public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
{
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 6434621c4..69c0d26b6 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -18,10 +18,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
- <PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
+ <PackageReference Include="System.Threading.Tasks.Dataflow" />
</ItemGroup>
<ItemGroup>
@@ -51,13 +51,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index e94a04a7d..5de57917e 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -38,7 +38,13 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
- private readonly Version _minKernelVersioni915Hang = new Version(5, 18);
+ // i915 hang was fixed by linux 6.2 (3f882f2)
+ private readonly Version _minKerneli915Hang = new Version(5, 18);
+ private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
+ private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
+
+ private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
+ private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -58,6 +64,31 @@ namespace MediaBrowser.Controller.MediaEncoding
"Main10"
};
+ private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "mp4",
+ "m4a",
+ "m4p",
+ "m4b",
+ "m4r",
+ "m4v",
+ };
+
+ // Set max transcoding channels for encoders that can't handle more than a set amount of channels
+ // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels
+ private static readonly Dictionary<string, int> _audioTranscodeChannelLookup = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "wmav2", 2 },
+ { "libmp3lame", 2 },
+ { "libfdk_aac", 6 },
+ { "aac_at", 6 },
+ { "ac3", 6 },
+ { "eac3", 6 },
+ { "dca", 6 },
+ { "mlp", 6 },
+ { "truehd", 6 },
+ };
+
public EncodingHelper(
IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
@@ -530,9 +561,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetInputPathArgument(EncodingJobInfo state)
{
- var mediaPath = state.MediaPath ?? string.Empty;
-
- return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource);
+ return state.MediaSource.VideoType switch
+ {
+ VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
+ VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
+ _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
+ };
}
/// <summary>
@@ -546,6 +580,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
{
+ // Use Apple's aac encoder if available as it provides best audio quality
+ if (_mediaEncoder.SupportsEncoder("aac_at"))
+ {
+ return "aac_at";
+ }
+
// Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support
if (_mediaEncoder.SupportsEncoder("libfdk_aac"))
{
@@ -605,6 +645,26 @@ namespace MediaBrowser.Controller.MediaEncoding
deviceIndex);
}
+ private string GetVulkanDeviceArgs(int deviceIndex, string deviceName, string srcDeviceAlias, string alias)
+ {
+ alias ??= VulkanAlias;
+ deviceIndex = deviceIndex >= 0
+ ? deviceIndex
+ : 0;
+ var vendorOpts = string.IsNullOrEmpty(deviceName)
+ ? ":" + deviceIndex
+ : ":" + "\"" + deviceName + "\"";
+ var options = string.IsNullOrEmpty(srcDeviceAlias)
+ ? vendorOpts
+ : "@" + srcDeviceAlias;
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ " -init_hw_device vulkan={0}{1}",
+ alias,
+ options);
+ }
+
private string GetOpenclDeviceArgs(int deviceIndex, string deviceVendorName, string srcDeviceAlias, string alias)
{
alias ??= OpenclAlias;
@@ -787,6 +847,12 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
+ else
+ {
+ // libplacebo wants an explicitly set vulkan filter device.
+ args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
+ }
}
else
{
@@ -928,8 +994,18 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(canvasArgs);
}
- arg.Append(" -i ")
- .Append(GetInputPathArgument(state));
+ if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
+ {
+ var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat");
+ _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
+ arg.Append(" -f concat -safe 0 -i ")
+ .Append(tmpConcatPath);
+ }
+ else
+ {
+ arg.Append(" -i ")
+ .Append(GetInputPathArgument(state));
+ }
// sub2video for external graphical subtitles
if (state.SubtitleStream is not null
@@ -1124,7 +1200,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
- if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel))
+ if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
{
if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
@@ -1336,7 +1412,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping.
var intelLowPowerHwEncoding = false;
- // Workaround for linux 5.18+ i915 hang at cost of performance.
+ // Workaround for linux 5.18 to 6.1.3 i915 hang at cost of performance.
// https://github.com/intel/media-driver/issues/1456
var enableWaFori915Hang = false;
@@ -1355,18 +1431,25 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
- if (OperatingSystem.IsLinux() && Environment.OSVersion.Version >= _minKernelVersioni915Hang)
+ if (OperatingSystem.IsLinux())
{
- var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty;
- var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)
- || vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
- var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv")
- && IsVaapiSupported(state)
- && IsOpenclFullSupported()
- && !IsVaapiVppTonemapAvailable(state, encodingOptions)
- && IsHwTonemapAvailable(state, encodingOptions);
+ var ver = Environment.OSVersion.Version;
+ var isFixedKernel60 = ver.Major == 6 && ver.Minor == 0 && ver >= _minFixedKernel60i915Hang;
+ var isUnaffectedKernel = ver < _minKerneli915Hang || ver > _maxKerneli915Hang;
- enableWaFori915Hang = isIntelDecoder && doOclTonemap;
+ if (!(isUnaffectedKernel || isFixedKernel60))
+ {
+ var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty;
+ var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)
+ || vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
+ var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv")
+ && IsVaapiSupported(state)
+ && IsOpenclFullSupported()
+ && !IsVaapiVppTonemapAvailable(state, encodingOptions)
+ && IsHwTonemapAvailable(state, encodingOptions);
+
+ enableWaFori915Hang = isIntelDecoder && doOclTonemap;
+ }
}
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
@@ -1711,7 +1794,7 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
// hevc_qsv use -level 51 instead of -level 153.
- if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel))
+ if (double.TryParse(level, CultureInfo.InvariantCulture, out double hevcLevel))
{
param += " -level " + (hevcLevel / 3);
}
@@ -1890,8 +1973,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a specific level was requested, the source must match or be less than
var level = state.GetRequestedLevel(videoStream.Codec);
- if (!string.IsNullOrEmpty(level)
- && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel))
+ if (double.TryParse(level, CultureInfo.InvariantCulture, out var requestLevel))
{
if (!videoStream.Level.HasValue)
{
@@ -2034,14 +2116,20 @@ namespace MediaBrowser.Controller.MediaEncoding
private static double GetVideoBitrateScaleFactor(string codec)
{
+ // hevc & vp9 - 40% more efficient than h.264
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return .6;
}
+ // av1 - 50% more efficient than h.264
+ if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return .5;
+ }
+
return 1;
}
@@ -2049,7 +2137,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
- var scaleFactor = outputScaleFactor / inputScaleFactor;
+
+ // Don't scale the real bitrate lower than the requested bitrate
+ var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1);
if (bitrate <= 500000)
{
@@ -2191,87 +2281,48 @@ namespace MediaBrowser.Controller.MediaEncoding
var request = state.BaseRequest;
- var inputChannels = audioStream.Channels;
+ var codec = outputAudioCodec ?? string.Empty;
- if (inputChannels <= 0)
- {
- inputChannels = null;
- }
+ int? resultChannels = state.GetRequestedAudioChannels(codec);
- var codec = outputAudioCodec ?? string.Empty;
+ var inputChannels = audioStream.Channels;
- int? transcoderChannelLimit;
- if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
- {
- // wmav2 currently only supports two channel output
- transcoderChannelLimit = 2;
- }
- else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
- {
- // libmp3lame currently only supports two channel output
- transcoderChannelLimit = 2;
- }
- else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
- {
- // aac is able to handle 8ch(7.1 layout)
- transcoderChannelLimit = 8;
- }
- else
+ if (inputChannels > 0)
{
- // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
- transcoderChannelLimit = 6;
+ resultChannels = inputChannels < resultChannels ? inputChannels : resultChannels ?? inputChannels;
}
var isTranscodingAudio = !IsCopyCodec(codec);
- int? resultChannels = state.GetRequestedAudioChannels(codec);
if (isTranscodingAudio)
{
- resultChannels = GetMinValue(request.TranscodingMaxAudioChannels, resultChannels);
- }
+ var audioEncoder = GetAudioEncoder(state);
+ if (!_audioTranscodeChannelLookup.TryGetValue(audioEncoder, out var transcoderChannelLimit))
+ {
+ // Set default max transcoding channels to 8 to prevent encoding errors due to asking for too many channels.
+ transcoderChannelLimit = 8;
+ }
- if (inputChannels.HasValue)
- {
- resultChannels = resultChannels.HasValue
- ? Math.Min(resultChannels.Value, inputChannels.Value)
- : inputChannels.Value;
- }
+ // Set resultChannels to minimum between resultChannels, TranscodingMaxAudioChannels, transcoderChannelLimit
+ resultChannels = transcoderChannelLimit < resultChannels ? transcoderChannelLimit : resultChannels ?? transcoderChannelLimit;
- if (isTranscodingAudio && transcoderChannelLimit.HasValue)
- {
- resultChannels = resultChannels.HasValue
- ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value)
- : transcoderChannelLimit.Value;
- }
+ if (request.TranscodingMaxAudioChannels < resultChannels)
+ {
+ resultChannels = request.TranscodingMaxAudioChannels;
+ }
- // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout).
- // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices
- if (isTranscodingAudio
- && state.TranscodingType != TranscodingJobType.Progressive
- && resultChannels.HasValue
- && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7))
- {
- resultChannels = 2;
+ // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout).
+ // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices
+ if (state.TranscodingType != TranscodingJobType.Progressive
+ && ((resultChannels > 2 && resultChannels < 6) || resultChannels == 7))
+ {
+ resultChannels = 2;
+ }
}
return resultChannels;
}
- private int? GetMinValue(int? val1, int? val2)
- {
- if (!val1.HasValue)
- {
- return val2;
- }
-
- if (!val2.HasValue)
- {
- return val1;
- }
-
- return Math.Min(val1.Value, val2.Value);
- }
-
/// <summary>
/// Enforces the resolution limit.
/// </summary>
@@ -2429,6 +2480,30 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
+ /// Gets the negative map args by filters.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="videoProcessFilters">The videoProcessFilters.</param>
+ /// <returns>System.String.</returns>
+ public string GetNegativeMapArgsByFilters(EncodingJobInfo state, string videoProcessFilters)
+ {
+ string args = string.Empty;
+
+ // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
+ if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
+ {
+ int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
+
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ "-map -0:{0} ",
+ videoStreamIndex);
+ }
+
+ return args;
+ }
+
+ /// <summary>
/// Determines which stream will be used for playback.
/// </summary>
/// <param name="allStream">All stream.</param>
@@ -2794,6 +2869,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return "deinterlace_qsv=mode=2";
}
+ else if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "yadif_videotoolbox={0}:-1:0",
+ doubleRateDeint ? "1" : "0");
+ }
return string.Empty;
}
@@ -2939,8 +3021,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (hasGraphicalSubs)
{
- // [0:s]scale=expr
- var subSwScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ // [0:s]scale=s=1280x720
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
}
@@ -3126,9 +3208,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = isSwDecoder
- ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
- : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
}
@@ -3265,7 +3345,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
- var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap";
+ var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap=mode=read";
mainFilters.Add(hwTransferFilter);
mainFilters.Add("format=nv12");
}
@@ -3328,9 +3408,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = isSwDecoder
- ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
- : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
}
@@ -3510,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
// qsv hwmap is not fully implemented for the time being.
- mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
+ mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@@ -3582,9 +3660,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = isSwDecoder
- ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
- : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
}
@@ -3670,6 +3746,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder)
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -3716,7 +3799,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
// qsv hwmap is not fully implemented for the time being.
- mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
+ mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@@ -3793,9 +3876,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = isSwDecoder
- ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
- : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
}
@@ -3947,6 +4028,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter))
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -3988,7 +4076,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
- mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap");
+ mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
mainFilters.Add("format=nv12");
}
@@ -4054,9 +4142,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = isSwDecoder
- ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
- : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
@@ -4128,7 +4214,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw
if (doVkTonemap)
{
- mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
+ mainFilters.Add("hwupload_vaapi");
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
}
}
else if (isVaapiDecoder)
@@ -4158,6 +4246,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
}
// vk tonemap
@@ -4234,12 +4323,14 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(subTextSubtitlesFilter);
}
- subFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
+ // prefer vaapi hwupload to vulkan hwupload,
+ // Mesa RADV does not support a dedicated transfer queue.
+ subFilters.Add("hwupload_vaapi");
+ subFilters.Add("hwmap=derive_device=vulkan");
+ subFilters.Add("format=vulkan");
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
-
- // explicitly sync using libplacebo.
- overlayFilters.Add("libplacebo=format=nv12:upscaler=none:downscaler=none");
+ overlayFilters.Add("scale_vulkan=format=nv12");
// OUTPUT vaapi(nv12/bgra) surface(vram)
// reverse-mapping via vaapi-vulkan interop.
@@ -4251,9 +4342,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = isSwDecoder
- ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
- : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
@@ -4340,6 +4429,13 @@ namespace MediaBrowser.Controller.MediaEncoding
outFormat = doOclTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter))
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -4428,9 +4524,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = isSwDecoder
- ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
- : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
@@ -4445,6 +4539,75 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
+ /// Gets the parameter of Apple VideoToolBox filter chain.
+ /// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="vidEncoder">Video encoder to use.</param>
+ /// <returns>The tuple contains three lists: main, sub and overlay filters.</returns>
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAppleVidFilterChain(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidEncoder)
+ {
+ if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ return (null, null, null);
+ }
+
+ var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder);
+
+ if (!options.EnableHardwareEncoding)
+ {
+ return swFilterChain;
+ }
+
+ if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0)
+ {
+ // All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg
+ return swFilterChain;
+ }
+
+ var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+ var doDeintH2645 = doDeintH264 || doDeintHevc;
+ var inW = state.VideoStream?.Width;
+ var inH = state.VideoStream?.Height;
+ var reqW = state.BaseRequest.Width;
+ var reqH = state.BaseRequest.Height;
+ var reqMaxW = state.BaseRequest.MaxWidth;
+ var reqMaxH = state.BaseRequest.MaxHeight;
+ var threeDFormat = state.MediaSource.Video3DFormat;
+ var newfilters = new List<string>();
+ var noOverlay = swFilterChain.OverlayFilters.Count == 0;
+ var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox");
+ // fallback to software filters if we are using filters not supported by hardware yet.
+ var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint);
+
+ if (!useHardwareFilters)
+ {
+ return swFilterChain;
+ }
+
+ // ffmpeg cannot use videotoolbox to scale
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ newfilters.Add(swScaleFilter);
+
+ // hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent
+ // videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here
+ // This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion
+ newfilters.Add("hwupload");
+
+ if (doDeintH2645)
+ {
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox");
+ newfilters.Add(deintFilter);
+ }
+
+ return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters);
+ }
+
+ /// <summary>
/// Gets the parameter of video processing filters.
/// </summary>
/// <param name="state">Encoding state.</param>
@@ -4486,6 +4649,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
(mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec);
}
+ else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec);
+ }
else
{
(mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec);
@@ -4646,7 +4813,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// HWA decoders can handle both video files and video folders.
- var videoType = mediaSource.VideoType;
+ var videoType = state.VideoType;
if (videoType != VideoType.VideoFile
&& videoType != VideoType.Iso
&& videoType != VideoType.Dvd
@@ -4787,8 +4954,18 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
+ var ffmpegVersion = _mediaEncoder.EncoderVersion;
+
// Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used.
- var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+ var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel
+ && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+
+ // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels.
+ var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase);
+
+ // Disable the extra internal copy in nvdec. We already handle it in filter chain.
+ var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput;
if (bitDepth == 10 && isCodecAvailable)
{
@@ -4814,14 +4991,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isVaapiSupported && isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
if (isD3d11Supported && isCodecAvailable)
{
// set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
// on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
- return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
}
}
else
@@ -4841,7 +5020,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (options.EnableEnhancedNvdecDecoder)
{
// set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support.
- return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty)
+ + (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
}
else
{
@@ -4856,7 +5036,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isD3d11Supported && isCodecAvailable)
{
- return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
}
@@ -4865,9 +5046,11 @@ namespace MediaBrowser.Controller.MediaEncoding
&& isVaapiSupported
&& isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
+ // Apple videotoolbox
if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)
&& isVideotoolboxSupported
&& isCodecAvailable)
@@ -5671,7 +5854,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// video processing filters.
var videoProcessParam = GetVideoProcessingFilterParam(state, encodingOptions, videoCodec);
- args += videoProcessParam;
+ var negativeMapArgs = GetNegativeMapArgsByFilters(state, videoProcessParam);
+
+ args = negativeMapArgs + args + videoProcessParam;
hasCopyTs = videoProcessParam.Contains("copyts", StringComparison.OrdinalIgnoreCase);
@@ -5766,6 +5951,11 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
+ if (!string.IsNullOrEmpty(state.OutputAudioCodec))
+ {
+ audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
+ }
+
if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
// opus only supports specific sampling rates
@@ -5785,6 +5975,13 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ // Copy the movflags from GetProgressiveVideoFullCommandLine
+ // See #9248 and the associated PR for why this is needed
+ if (_mp4ContainerNames.Contains(state.OutputContainer))
+ {
+ audioTranscodeParams.Add("-movflags empty_moov+delay_moov");
+ }
+
var threads = GetNumberOfThreads(state, encodingOptions, null);
var inputModifier = GetInputModifier(state, encodingOptions, null);
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 179cabc84..a6b541660 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -250,8 +250,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var level = GetRequestedLevel(ActualOutputVideoCodec);
- if (!string.IsNullOrEmpty(level)
- && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ if (double.TryParse(level, CultureInfo.InvariantCulture, out var result))
{
return result;
}
@@ -645,8 +644,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec))
{
var value = BaseRequest.GetOption(codec, "maxrefframes");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
@@ -665,8 +663,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec))
{
var value = BaseRequest.GetOption(codec, "videobitdepth");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
@@ -685,8 +682,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec))
{
var value = BaseRequest.GetOption(codec, "audiobitdepth");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
@@ -700,8 +696,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec))
{
var value = BaseRequest.GetOption(codec, "audiochannels");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index fe8e9063e..f830b9f29 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -154,6 +154,14 @@ namespace MediaBrowser.Controller.MediaEncoding
string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
/// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <returns>System.String.</returns>
+ string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource);
+
+ /// <summary>
/// Gets the input argument for an external subtitle file.
/// </summary>
/// <param name="inputFile">The input file.</param>
@@ -194,6 +202,20 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="path">The to the .vob files.</param>
/// <param name="titleNumber">The title number to start with.</param>
/// <returns>A playlist.</returns>
- IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
+ IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
+
+ /// <summary>
+ /// Gets the primary playlist of .m2ts files.
+ /// </summary>
+ /// <param name="path">The to the .m2ts files.</param>
+ /// <returns>A playlist.</returns>
+ IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
+
+ /// <summary>
+ /// Generates a FFmpeg concat config for the source.
+ /// </summary>
+ /// <param name="source">The <see cref="MediaSourceInfo"/>.</param>
+ /// <param name="concatFilePath">The path the config should be written to.</param>
+ void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index d8475f12a..3b34af4e9 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var rate = parts[i + 1];
- if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
+ if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
{
framerate = val;
}
@@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var rate = part.Split('=', 2)[^1];
- if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
+ if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
{
framerate = val;
}
@@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (scale.HasValue)
{
- if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
+ if (long.TryParse(size, CultureInfo.InvariantCulture, out var val))
{
bytesTranscoded = val * scale.Value;
}
@@ -146,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (scale.HasValue)
{
- if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
+ if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
{
bitRate = (int)Math.Ceiling(val * scale.Value);
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index fc9ea37d1..0524999c7 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -232,6 +232,11 @@ namespace MediaBrowser.Controller.Net
// TODO Investigate and properly fix.
Logger.LogError(ex, "Object Disposed");
}
+ catch (Exception ex)
+ {
+ // TODO Investigate and properly fix.
+ Logger.LogError(ex, "Error disposing websocket");
+ }
lock (_activeConnections)
{
diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs
deleted file mode 100644
index 7caba1097..000000000
--- a/MediaBrowser.Controller/Notifications/INotificationManager.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-
-namespace MediaBrowser.Controller.Notifications
-{
- public interface INotificationManager
- {
- /// <summary>
- /// Sends the notification.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendNotification(NotificationRequest request, CancellationToken cancellationToken);
-
- Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken);
-
- /// <summary>
- /// Adds the parts.
- /// </summary>
- /// <param name="services">The services.</param>
- /// <param name="notificationTypeFactories">The notification type factories.</param>
- void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories);
-
- /// <summary>
- /// Gets the notification types.
- /// </summary>
- /// <returns>IEnumerable{NotificationTypeInfo}.</returns>
- List<NotificationTypeInfo> GetNotificationTypes();
-
- /// <summary>
- /// Gets the notification services.
- /// </summary>
- /// <returns>IEnumerable{NotificationServiceInfo}.</returns>
- IEnumerable<NameIdPair> GetNotificationServices();
- }
-}
diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs
deleted file mode 100644
index 535c08795..000000000
--- a/MediaBrowser.Controller/Notifications/INotificationService.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-
-namespace MediaBrowser.Controller.Notifications
-{
- public interface INotificationService
- {
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- string Name { get; }
-
- /// <summary>
- /// Sends the notification.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendNotification(UserNotification request, CancellationToken cancellationToken);
-
- /// <summary>
- /// Determines whether [is enabled for user] [the specified user identifier].
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns><c>true</c> if [is enabled for user] [the specified user identifier]; otherwise, <c>false</c>.</returns>
- bool IsEnabledForUser(User user);
- }
-}
diff --git a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs
deleted file mode 100644
index 52a3e120b..000000000
--- a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.Notifications;
-
-namespace MediaBrowser.Controller.Notifications
-{
- public interface INotificationTypeFactory
- {
- /// <summary>
- /// Gets the notification types.
- /// </summary>
- /// <returns>IEnumerable{NotificationTypeInfo}.</returns>
- IEnumerable<NotificationTypeInfo> GetNotificationTypes();
- }
-}
diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs
deleted file mode 100644
index 4be0e09ae..000000000
--- a/MediaBrowser.Controller/Notifications/UserNotification.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Model.Notifications;
-
-namespace MediaBrowser.Controller.Notifications
-{
- public class UserNotification
- {
- public string Name { get; set; }
-
- public string Description { get; set; }
-
- public string Url { get; set; }
-
- public NotificationLevel Level { get; set; }
-
- public DateTime Date { get; set; }
-
- public User User { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 24f7b5cd3..2c52b2b45 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Persistence
/// </summary>
/// <param name="items">The items.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken);
+ void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken);
void SaveImages(BaseItem item);
diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
index b59a03738..c4ad352a3 100644
--- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs
+++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
@@ -12,10 +12,13 @@ namespace MediaBrowser.Controller.Providers
public EpisodeInfo()
{
SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SeasonProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public Dictionary<string, string> SeriesProviderIds { get; set; }
+ public Dictionary<string, string> SeasonProviderIds { get; set; }
+
public int? IndexNumberEnd { get; set; }
public bool IsMissingEpisode { get; set; }
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index 8a3709462..9e91a8bcd 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Providers
ReplaceAllMetadata = copy.ReplaceAllMetadata;
EnableRemoteContentProbe = copy.EnableRemoteContentProbe;
+ IsAutomated = copy.IsAutomated;
ImageRefreshMode = copy.ImageRefreshMode;
ReplaceAllImages = copy.ReplaceAllImages;
ReplaceImages = copy.ReplaceImages;
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index b38ee1146..c8b29aa1f 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index 52aa44024..b86e48243 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -20,12 +18,6 @@ namespace MediaBrowser.Controller.Subtitles
event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure;
/// <summary>
- /// Adds the parts.
- /// </summary>
- /// <param name="subtitleProviders">The subtitle providers.</param>
- void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders);
-
- /// <summary>
/// Searches the subtitles.
/// </summary>
/// <param name="video">The video.</param>
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index de3987b1e..71cdea529 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -22,13 +22,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index 1030cf055..c8912807e 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -169,12 +169,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
var text = reader.ReadElementContentAsString();
- if (!string.IsNullOrEmpty(text))
+ if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
{
- if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
- {
- item.CriticRating = value;
- }
+ item.CriticRating = value;
}
break;
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index db177ff76..80091bf5a 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -301,10 +302,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
var processArgs = string.Format(
CultureInfo.InvariantCulture,
- "-dump_attachment:{1} {2} -i {0} -t 0 -f null null",
+ "-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
inputPath,
attachmentStreamIndex,
- outputPath);
+ EncodingUtils.NormalizePath(outputPath));
int exitCode;
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index 7e026b42e..ea520b1d6 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -1,83 +1,123 @@
-#pragma warning disable CS1591
-
using System;
+using System.IO;
using System.Linq;
using BDInfo.IO;
using MediaBrowser.Model.IO;
-namespace MediaBrowser.MediaEncoding.BdInfo
+namespace MediaBrowser.MediaEncoding.BdInfo;
+
+/// <summary>
+/// Class BdInfoDirectoryInfo.
+/// </summary>
+public class BdInfoDirectoryInfo : IDirectoryInfo
{
- public class BdInfoDirectoryInfo : IDirectoryInfo
- {
- private readonly IFileSystem _fileSystem;
+ private readonly IFileSystem _fileSystem;
- private readonly FileSystemMetadata _impl;
+ private readonly FileSystemMetadata _impl;
- public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
- {
- _fileSystem = fileSystem;
- _impl = _fileSystem.GetDirectoryInfo(path);
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BdInfoDirectoryInfo" /> class.
+ /// </summary>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="path">The path.</param>
+ public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
+ {
+ _fileSystem = fileSystem;
+ _impl = _fileSystem.GetDirectoryInfo(path);
+ }
- private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
- {
- _fileSystem = fileSystem;
- _impl = impl;
- }
+ private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
+ {
+ _fileSystem = fileSystem;
+ _impl = impl;
+ }
- public string Name => _impl.Name;
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ public string Name => _impl.Name;
- public string FullName => _impl.FullName;
+ /// <summary>
+ /// Gets the full name.
+ /// </summary>
+ public string FullName => _impl.FullName;
- public IDirectoryInfo? Parent
+ /// <summary>
+ /// Gets the parent directory information.
+ /// </summary>
+ public IDirectoryInfo? Parent
+ {
+ get
{
- get
+ var parentFolder = Path.GetDirectoryName(_impl.FullName);
+ if (parentFolder is not null)
{
- var parentFolder = System.IO.Path.GetDirectoryName(_impl.FullName);
- if (parentFolder is not null)
- {
- return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
- }
-
- return null;
+ return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
}
- }
- public IDirectoryInfo[] GetDirectories()
- {
- return Array.ConvertAll(
- _fileSystem.GetDirectories(_impl.FullName).ToArray(),
- x => new BdInfoDirectoryInfo(_fileSystem, x));
+ return null;
}
+ }
- public IFileInfo[] GetFiles()
- {
- return Array.ConvertAll(
- _fileSystem.GetFiles(_impl.FullName).ToArray(),
- x => new BdInfoFileInfo(x));
- }
+ /// <summary>
+ /// Gets the directories.
+ /// </summary>
+ /// <returns>An array with all directories.</returns>
+ public IDirectoryInfo[] GetDirectories()
+ {
+ return _fileSystem.GetDirectories(_impl.FullName)
+ .Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
+ .ToArray();
+ }
- public IFileInfo[] GetFiles(string searchPattern)
- {
- return Array.ConvertAll(
- _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
- x => new BdInfoFileInfo(x));
- }
+ /// <summary>
+ /// Gets the files.
+ /// </summary>
+ /// <returns>All files of the directory.</returns>
+ public IFileInfo[] GetFiles()
+ {
+ return _fileSystem.GetFiles(_impl.FullName)
+ .Select(x => new BdInfoFileInfo(x))
+ .ToArray();
+ }
- public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption)
- {
- return Array.ConvertAll(
- _fileSystem.GetFiles(
- _impl.FullName,
- new[] { searchPattern },
- false,
- (searchOption & System.IO.SearchOption.AllDirectories) == System.IO.SearchOption.AllDirectories).ToArray(),
- x => new BdInfoFileInfo(x));
- }
+ /// <summary>
+ /// Gets the files matching a pattern.
+ /// </summary>
+ /// <param name="searchPattern">The search pattern.</param>
+ /// <returns>All files of the directory matchign the search pattern.</returns>
+ public IFileInfo[] GetFiles(string searchPattern)
+ {
+ return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
+ .Select(x => new BdInfoFileInfo(x))
+ .ToArray();
+ }
- public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
- {
- return new BdInfoDirectoryInfo(fs, path);
- }
+ /// <summary>
+ /// Gets the files matching a pattern and search options.
+ /// </summary>
+ /// <param name="searchPattern">The search pattern.</param>
+ /// <param name="searchOption">The search optin.</param>
+ /// <returns>All files of the directory matchign the search pattern and options.</returns>
+ public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption)
+ {
+ return _fileSystem.GetFiles(
+ _impl.FullName,
+ new[] { searchPattern },
+ false,
+ (searchOption & SearchOption.AllDirectories) == SearchOption.AllDirectories)
+ .Select(x => new BdInfoFileInfo(x))
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Gets the bdinfo of a file system path.
+ /// </summary>
+ /// <param name="fs">The file system.</param>
+ /// <param name="path">The path.</param>
+ /// <returns>The BD directory information of the path on the file system.</returns>
+ public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
+ {
+ return new BdInfoDirectoryInfo(fs, path);
}
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
index 3e53cbf29..8ebb59c59 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
@@ -6,189 +6,182 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-namespace MediaBrowser.MediaEncoding.BdInfo
+namespace MediaBrowser.MediaEncoding.BdInfo;
+
+/// <summary>
+/// Class BdInfoExaminer.
+/// </summary>
+public class BdInfoExaminer : IBlurayExaminer
{
+ private readonly IFileSystem _fileSystem;
+
/// <summary>
- /// Class BdInfoExaminer.
+ /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
/// </summary>
- public class BdInfoExaminer : IBlurayExaminer
+ /// <param name="fileSystem">The filesystem.</param>
+ public BdInfoExaminer(IFileSystem fileSystem)
{
- private readonly IFileSystem _fileSystem;
+ _fileSystem = fileSystem;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
- /// </summary>
- /// <param name="fileSystem">The filesystem.</param>
- public BdInfoExaminer(IFileSystem fileSystem)
+ /// <summary>
+ /// Gets the disc info.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>BlurayDiscInfo.</returns>
+ public BlurayDiscInfo GetDiscInfo(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
{
- _fileSystem = fileSystem;
+ throw new ArgumentNullException(nameof(path));
}
- /// <summary>
- /// Gets the disc info.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>BlurayDiscInfo.</returns>
- public BlurayDiscInfo GetDiscInfo(string path)
- {
- if (string.IsNullOrWhiteSpace(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path));
+ var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path));
- bdrom.Scan();
+ bdrom.Scan();
- // Get the longest playlist
- var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
+ // Get the longest playlist
+ var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
- var outputStream = new BlurayDiscInfo
- {
- MediaStreams = Array.Empty<MediaStream>()
- };
+ var outputStream = new BlurayDiscInfo
+ {
+ MediaStreams = Array.Empty<MediaStream>()
+ };
- if (playlist is null)
- {
- return outputStream;
- }
+ if (playlist is null)
+ {
+ return outputStream;
+ }
- outputStream.Chapters = playlist.Chapters.ToArray();
+ outputStream.Chapters = playlist.Chapters.ToArray();
- outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
+ outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
- var mediaStreams = new List<MediaStream>();
+ var sortedStreams = playlist.SortedStreams;
+ var mediaStreams = new List<MediaStream>(sortedStreams.Count);
- foreach (var stream in playlist.SortedStreams)
+ foreach (var stream in sortedStreams)
+ {
+ switch (stream)
{
- if (stream is TSVideoStream videoStream)
- {
+ case TSVideoStream videoStream:
AddVideoStream(mediaStreams, videoStream);
- continue;
- }
-
- if (stream is TSAudioStream audioStream)
- {
+ break;
+ case TSAudioStream audioStream:
AddAudioStream(mediaStreams, audioStream);
- continue;
- }
-
- if (stream is TSTextStream textStream)
- {
+ break;
+ case TSTextStream textStream:
AddSubtitleStream(mediaStreams, textStream);
- continue;
- }
-
- if (stream is TSGraphicsStream graphicsStream)
- {
- AddSubtitleStream(mediaStreams, graphicsStream);
- }
+ break;
+ case TSGraphicsStream graphicStream:
+ AddSubtitleStream(mediaStreams, graphicStream);
+ break;
}
+ }
- outputStream.MediaStreams = mediaStreams.ToArray();
-
- outputStream.PlaylistName = playlist.Name;
+ outputStream.MediaStreams = mediaStreams.ToArray();
- if (playlist.StreamClips is not null && playlist.StreamClips.Any())
- {
- // Get the files in the playlist
- outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray();
- }
+ outputStream.PlaylistName = playlist.Name;
- return outputStream;
+ if (playlist.StreamClips is not null && playlist.StreamClips.Count > 0)
+ {
+ // Get the files in the playlist
+ outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray();
}
- /// <summary>
- /// Adds the video stream.
- /// </summary>
- /// <param name="streams">The streams.</param>
- /// <param name="videoStream">The video stream.</param>
- private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
- {
- var mediaStream = new MediaStream
- {
- BitRate = Convert.ToInt32(videoStream.BitRate),
- Width = videoStream.Width,
- Height = videoStream.Height,
- Codec = videoStream.CodecShortName,
- IsInterlaced = videoStream.IsInterlaced,
- Type = MediaStreamType.Video,
- Index = streams.Count
- };
-
- if (videoStream.FrameRateDenominator > 0)
- {
- float frameRateEnumerator = videoStream.FrameRateEnumerator;
- float frameRateDenominator = videoStream.FrameRateDenominator;
+ return outputStream;
+ }
- mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
- }
+ /// <summary>
+ /// Adds the video stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="videoStream">The video stream.</param>
+ private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
+ {
+ var mediaStream = new MediaStream
+ {
+ BitRate = Convert.ToInt32(videoStream.BitRate),
+ Width = videoStream.Width,
+ Height = videoStream.Height,
+ Codec = videoStream.CodecShortName,
+ IsInterlaced = videoStream.IsInterlaced,
+ Type = MediaStreamType.Video,
+ Index = streams.Count
+ };
+
+ if (videoStream.FrameRateDenominator > 0)
+ {
+ float frameRateEnumerator = videoStream.FrameRateEnumerator;
+ float frameRateDenominator = videoStream.FrameRateDenominator;
- streams.Add(mediaStream);
+ mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
}
- /// <summary>
- /// Adds the audio stream.
- /// </summary>
- /// <param name="streams">The streams.</param>
- /// <param name="audioStream">The audio stream.</param>
- private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
- {
- var stream = new MediaStream
- {
- Codec = audioStream.CodecShortName,
- Language = audioStream.LanguageCode,
- Channels = audioStream.ChannelCount,
- SampleRate = audioStream.SampleRate,
- Type = MediaStreamType.Audio,
- Index = streams.Count
- };
-
- var bitrate = Convert.ToInt32(audioStream.BitRate);
+ streams.Add(mediaStream);
+ }
- if (bitrate > 0)
- {
- stream.BitRate = bitrate;
- }
+ /// <summary>
+ /// Adds the audio stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="audioStream">The audio stream.</param>
+ private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
+ {
+ var stream = new MediaStream
+ {
+ Codec = audioStream.CodecShortName,
+ Language = audioStream.LanguageCode,
+ Channels = audioStream.ChannelCount,
+ SampleRate = audioStream.SampleRate,
+ Type = MediaStreamType.Audio,
+ Index = streams.Count
+ };
- if (audioStream.LFE > 0)
- {
- stream.Channels = audioStream.ChannelCount + 1;
- }
+ var bitrate = Convert.ToInt32(audioStream.BitRate);
- streams.Add(stream);
+ if (bitrate > 0)
+ {
+ stream.BitRate = bitrate;
}
- /// <summary>
- /// Adds the subtitle stream.
- /// </summary>
- /// <param name="streams">The streams.</param>
- /// <param name="textStream">The text stream.</param>
- private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
+ if (audioStream.LFE > 0)
{
- streams.Add(new MediaStream
- {
- Language = textStream.LanguageCode,
- Codec = textStream.CodecShortName,
- Type = MediaStreamType.Subtitle,
- Index = streams.Count
- });
+ stream.Channels = audioStream.ChannelCount + 1;
}
- /// <summary>
- /// Adds the subtitle stream.
- /// </summary>
- /// <param name="streams">The streams.</param>
- /// <param name="textStream">The text stream.</param>
- private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
+ streams.Add(stream);
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
+ {
+ streams.Add(new MediaStream
{
- streams.Add(new MediaStream
- {
- Language = textStream.LanguageCode,
- Codec = textStream.CodecShortName,
- Type = MediaStreamType.Subtitle,
- Index = streams.Count
- });
- }
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
}
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index d55688e3d..9e7a1d50a 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -1,41 +1,68 @@
-#pragma warning disable CS1591
-
using System.IO;
using MediaBrowser.Model.IO;
-namespace MediaBrowser.MediaEncoding.BdInfo
+namespace MediaBrowser.MediaEncoding.BdInfo;
+
+/// <summary>
+/// Class BdInfoFileInfo.
+/// </summary>
+public class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
- public class BdInfoFileInfo : BDInfo.IO.IFileInfo
- {
- private FileSystemMetadata _impl;
+ private FileSystemMetadata _impl;
- public BdInfoFileInfo(FileSystemMetadata impl)
- {
- _impl = impl;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class.
+ /// </summary>
+ /// <param name="impl">The <see cref="FileSystemMetadata" />.</param>
+ public BdInfoFileInfo(FileSystemMetadata impl)
+ {
+ _impl = impl;
+ }
- public string Name => _impl.Name;
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ public string Name => _impl.Name;
- public string FullName => _impl.FullName;
+ /// <summary>
+ /// Gets the full name.
+ /// </summary>
+ public string FullName => _impl.FullName;
- public string Extension => _impl.Extension;
+ /// <summary>
+ /// Gets the extension.
+ /// </summary>
+ public string Extension => _impl.Extension;
- public long Length => _impl.Length;
+ /// <summary>
+ /// Gets the length.
+ /// </summary>
+ public long Length => _impl.Length;
- public bool IsDir => _impl.IsDirectory;
+ /// <summary>
+ /// Gets a value indicating whether this is a directory.
+ /// </summary>
+ public bool IsDir => _impl.IsDirectory;
- public Stream OpenRead()
- {
- return new FileStream(
- FullName,
- FileMode.Open,
- FileAccess.Read,
- FileShare.Read);
- }
+ /// <summary>
+ /// Gets a file as file stream.
+ /// </summary>
+ /// <returns>A <see cref="FileStream" /> for the file.</returns>
+ public Stream OpenRead()
+ {
+ return new FileStream(
+ FullName,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read);
+ }
- public StreamReader OpenText()
- {
- return new StreamReader(OpenRead());
- }
+ /// <summary>
+ /// Gets a files's content with a stream reader.
+ /// </summary>
+ /// <returns>A <see cref="StreamReader" /> for the file's content.</returns>
+ public StreamReader OpenText()
+ {
+ return new StreamReader(OpenRead());
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 8479b7d50..540d50bf1 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -56,6 +56,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"libvpx",
"libvpx-vp9",
"aac",
+ "aac_at",
"libfdk_aac",
"ac3",
"libmp3lame",
@@ -106,7 +107,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
// vulkan
"libplacebo",
"scale_vulkan",
- "overlay_vulkan"
+ "overlay_vulkan",
+ "hwupload_vaapi",
+ // videotoolbox
+ "yadif_videotoolbox"
};
private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
@@ -273,7 +277,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (match.Success)
{
- if (Version.TryParse(match.Groups[1].Value, out var result))
+ if (Version.TryParse(match.Groups[1].ValueSpan, out var result))
{
return result;
}
@@ -323,8 +327,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
RegexOptions.Multiline))
{
var version = new Version(
- int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture),
- int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture));
+ int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),
+ int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture));
map.Add(match.Groups["name"].Value, version);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index d0ea0429b..04128c911 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -1,7 +1,9 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Encoder
@@ -15,21 +17,38 @@ namespace MediaBrowser.MediaEncoding.Encoder
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile);
}
- return GetConcatInputArgument(inputFile, inputPrefix);
+ return GetFileInputArgument(inputFile, inputPrefix);
+ }
+
+ public static string GetInputArgument(string inputPrefix, IReadOnlyList<string> inputFiles, MediaProtocol protocol)
+ {
+ if (protocol != MediaProtocol.File)
+ {
+ return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFiles[0]);
+ }
+
+ return GetConcatInputArgument(inputFiles, inputPrefix);
}
/// <summary>
/// Gets the concat input argument.
/// </summary>
- /// <param name="inputFile">The input file.</param>
+ /// <param name="inputFiles">The input files.</param>
/// <param name="inputPrefix">The input prefix.</param>
/// <returns>System.String.</returns>
- private static string GetConcatInputArgument(string inputFile, string inputPrefix)
+ private static string GetConcatInputArgument(IReadOnlyList<string> inputFiles, string inputPrefix)
{
// Get all streams
// If there's more than one we'll need to use the concat command
+ if (inputFiles.Count > 1)
+ {
+ var files = string.Join("|", inputFiles.Select(NormalizePath));
+
+ return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
+ }
+
// Determine the input path for video files
- return GetFileInputArgument(inputFile, inputPrefix);
+ return GetFileInputArgument(inputFiles[0], inputPrefix);
}
/// <summary>
@@ -56,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.String.</returns>
- private static string NormalizePath(string path)
+ public static string NormalizePath(string path)
{
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
return path.Replace("\"", "\\\"", StringComparison.Ordinal);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index d95f894c5..d2112e5dc 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -11,7 +11,9 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -50,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
+ private readonly IBlurayExaminer _blurayExaminer;
private readonly IConfiguration _config;
private readonly IServerConfigurationManager _serverConfig;
private readonly string _startupOptionFFmpegPath;
@@ -94,6 +97,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
ILogger<MediaEncoder> logger,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
+ IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IConfiguration config,
IServerConfigurationManager serverConfig)
@@ -101,11 +105,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
+ _blurayExaminer = blurayExaminer;
_localization = localization;
_config = config;
_serverConfig = serverConfig;
_startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
- _jsonSerializerOptions = JsonDefaults.Options;
+
+ _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
+ _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
}
/// <inheritdoc />
@@ -114,16 +121,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public string ProbePath => _ffprobePath;
+ /// <inheritdoc />
public Version EncoderVersion => _ffmpegVersion;
+ /// <inheritdoc />
public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
+ /// <inheritdoc />
public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
+ /// <inheritdoc />
public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
+ /// <inheritdoc />
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
+ /// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
/// <summary>
@@ -341,26 +354,31 @@ namespace MediaBrowser.MediaEncoding.Encoder
_ffmpegVersion = validator.GetFFmpegVersion();
}
+ /// <inheritdoc />
public bool SupportsEncoder(string encoder)
{
return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsDecoder(string decoder)
{
return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsHwaccel(string hwaccel)
{
return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsFilter(string filter)
{
return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public bool SupportsFilterWithOption(FilterOptionType option)
{
if (_filtersWithOption.TryGetValue((int)option, out var val))
@@ -391,24 +409,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
return true;
}
- /// <summary>
- /// Gets the media info.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
+ /// <inheritdoc />
public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
{
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
- var inputFile = request.MediaSource.Path;
-
string analyzeDuration = string.Empty;
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (request.MediaSource.AnalyzeDurationMs > 0)
{
- analyzeDuration = "-analyzeduration " +
- (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
+ analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
}
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
{
@@ -416,7 +426,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
return GetMediaInfoInternal(
- GetInputArgument(inputFile, request.MediaSource),
+ GetInputArgument(request.MediaSource.Path, request.MediaSource),
request.MediaSource.Path,
request.MediaSource.Protocol,
extractChapters,
@@ -426,36 +436,30 @@ namespace MediaBrowser.MediaEncoding.Encoder
cancellationToken);
}
- /// <summary>
- /// Gets the input argument.
- /// </summary>
- /// <param name="inputFile">The input file.</param>
- /// <param name="mediaSource">The mediaSource.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentException">Unrecognized InputType.</exception>
+ /// <inheritdoc />
+ public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
+ {
+ return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
+ }
+
+ /// <inheritdoc />
public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
{
var prefix = "file";
- if (mediaSource.VideoType == VideoType.BluRay
- || mediaSource.IsoType == IsoType.BluRay)
+ if (mediaSource.IsoType == IsoType.BluRay)
{
prefix = "bluray";
}
- return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol);
+ return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
}
- /// <summary>
- /// Gets the input argument for an external subtitle file.
- /// </summary>
- /// <param name="inputFile">The input file.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentException">Unrecognized InputType.</exception>
+ /// <inheritdoc />
public string GetExternalSubtitleInputArgument(string inputFile)
{
const string Prefix = "file";
- return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File);
+ return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
}
/// <summary>
@@ -546,6 +550,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ /// <inheritdoc />
public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
{
var mediaSource = new MediaSourceInfo
@@ -556,11 +561,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken);
}
+ /// <inheritdoc />
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken);
}
+ /// <inheritdoc />
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken);
@@ -764,6 +771,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ /// <inheritdoc />
public string GetTimeParameter(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
@@ -863,82 +871,111 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
/// <inheritdoc />
- public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
+ public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
{
- // min size 300 mb
- const long MinPlayableSize = 314572800;
-
- // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size
- // Once we reach a file that is at least the minimum, return all subsequent ones
+ // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VOB
var allVobs = _fileSystem.GetFiles(path, true)
- .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase))
+ .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
+ .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
+ .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
.OrderBy(i => i.FullName)
.ToList();
- // If we didn't find any satisfying the min length, just take them all
- if (allVobs.Count == 0)
- {
- _logger.LogWarning("No vobs found in dvd structure.");
- return Enumerable.Empty<string>();
- }
-
if (titleNumber.HasValue)
{
- var prefix = string.Format(
- CultureInfo.InvariantCulture,
- titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_",
- titleNumber.Value);
+ var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
if (vobs.Count > 0)
{
- var minSizeVobs = vobs
- .SkipWhile(f => f.Length < MinPlayableSize)
- .ToList();
-
- return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName);
+ return vobs.Select(i => i.FullName).ToList();
}
- _logger.LogWarning("Could not determine vob file list for {Path} using DvdLib. Will scan using file sizes.", path);
+ _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
}
- var files = allVobs
- .SkipWhile(f => f.Length < MinPlayableSize)
+ // Check for multiple big titles (> 900 MB)
+ var titles = allVobs
+ .Where(vob => vob.Length >= 900 * 1024 * 1024)
+ .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
+ .Distinct()
.ToList();
- // If we didn't find any satisfying the min length, just take them all
- if (files.Count == 0)
+ // Fall back to first title if no big title is found
+ if (titles.Count == 0)
{
- _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs.");
- files = allVobs;
+ titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
}
- // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file
- if (files.Count > 0)
- {
- var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_');
+ // Aggregate all .vob files of the titles
+ return allVobs
+ .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()))
+ .Select(i => i.FullName)
+ .ToList();
+ }
- if (parts.Length == 3)
- {
- var title = parts[1];
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
+ {
+ // Get all playable .m2ts files
+ var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
- files = files.TakeWhile(f =>
- {
- var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_');
+ // Get all files from the BDMV/STREAMING directory
+ var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
- return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase);
- }).ToList();
+ // Only return playable local .m2ts files
+ return directoryFiles
+ .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
+ .Select(f => f.FullName)
+ .ToList();
+ }
- // If this resulted in not getting any vobs, just take them all
- if (files.Count == 0)
- {
- _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs.");
- files = allVobs;
- }
- }
+ /// <inheritdoc />
+ public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
+ {
+ // Get all playable files
+ IReadOnlyList<string> files;
+ var videoType = source.VideoType;
+ if (videoType == VideoType.Dvd)
+ {
+ files = GetPrimaryPlaylistVobFiles(source.Path, null);
+ }
+ else if (videoType == VideoType.BluRay)
+ {
+ files = GetPrimaryPlaylistM2tsFiles(source.Path);
+ }
+ else
+ {
+ return;
}
- return files.Select(i => i.FullName);
+ // Generate concat configuration entries for each file and write to file
+ using (StreamWriter sw = new StreamWriter(concatFilePath))
+ {
+ foreach (var path in files)
+ {
+ var mediaInfoResult = GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaType = DlnaProfileType.Video,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = path,
+ Protocol = MediaProtocol.File,
+ VideoType = videoType
+ }
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
+
+ var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
+
+ // Add file path stanza to concat configuration
+ sw.WriteLine("file '{0}'", path);
+
+ // Add duration stanza to concat configuration
+ sw.WriteLine("duration {0}", duration);
+ }
+ }
}
public bool CanExtractSubtitles(string codec)
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index e33cfc7a1..a0624fe76 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -22,22 +22,22 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BDInfo" Version="0.7.6.2" />
- <PackageReference Include="libse" Version="3.6.10" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
- <PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
- <PackageReference Include="UTF.Unknown" Version="2.5.1" />
+ <PackageReference Include="BDInfo" />
+ <PackageReference Include="libse" />
+ <PackageReference Include="Microsoft.Extensions.Http" />
+ <PackageReference Include="System.Text.Encoding.CodePages" />
+ <PackageReference Include="UTF.Unknown" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.MediaEncoding/Probing/CodecType.cs b/MediaBrowser.MediaEncoding/Probing/CodecType.cs
new file mode 100644
index 000000000..d7c68e5f3
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Probing/CodecType.cs
@@ -0,0 +1,32 @@
+namespace MediaBrowser.MediaEncoding.Probing;
+
+/// <summary>
+/// FFmpeg Codec Type.
+/// </summary>
+public enum CodecType
+{
+ /// <summary>
+ /// Video.
+ /// </summary>
+ Video,
+
+ /// <summary>
+ /// Audio.
+ /// </summary>
+ Audio,
+
+ /// <summary>
+ /// Opaque data information usually continuous.
+ /// </summary>
+ Data,
+
+ /// <summary>
+ /// Subtitles.
+ /// </summary>
+ Subtitle,
+
+ /// <summary>
+ /// Opaque data information usually sparse.
+ /// </summary>
+ Attachment
+}
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index eab8f79bb..294442324 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -43,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The codec_type.</value>
[JsonPropertyName("codec_type")]
- public string CodecType { get; set; }
+ public CodecType CodecType { get; set; }
/// <summary>
/// Gets or sets the sample_rate.
@@ -228,11 +228,11 @@ namespace MediaBrowser.MediaEncoding.Probing
public long StartPts { get; set; }
/// <summary>
- /// Gets or sets the is_avc.
+ /// Gets or sets a value indicating whether the stream is AVC.
/// </summary>
/// <value>The is_avc.</value>
[JsonPropertyName("is_avc")]
- public string IsAvc { get; set; }
+ public bool IsAvc { get; set; }
/// <summary>
/// Gets or sets the nal_length_size.
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 18e248a1b..cb482301f 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -65,7 +65,8 @@ namespace MediaBrowser.MediaEncoding.Probing
"K/DA",
"22/7",
"諭吉佳作/men",
- "//dARTH nULL"
+ "//dARTH nULL",
+ "Phantom/Ghost",
};
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
@@ -96,19 +97,16 @@ namespace MediaBrowser.MediaEncoding.Probing
{
info.Container = NormalizeFormat(data.Format.FormatName);
- if (!string.IsNullOrEmpty(data.Format.BitRate))
+ if (int.TryParse(data.Format.BitRate, CultureInfo.InvariantCulture, out var value))
{
- if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
- {
- info.Bitrate = value;
- }
+ info.Bitrate = value;
}
}
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- var tagStreamType = isAudio ? "audio" : "video";
+ var tagStreamType = isAudio ? CodecType.Audio : CodecType.Video;
- var tagStream = data.Streams?.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase));
+ var tagStream = data.Streams?.FirstOrDefault(i => i.CodecType == tagStreamType);
if (tagStream?.Tags is not null)
{
@@ -250,12 +248,23 @@ namespace MediaBrowser.MediaEncoding.Probing
return null;
}
+ // Handle MPEG-1 container
if (string.Equals(format, "mpegvideo", StringComparison.OrdinalIgnoreCase))
{
return "mpeg";
}
- format = format.Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
+ // Handle MPEG-2 container
+ if (string.Equals(format, "mpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "ts";
+ }
+
+ // Handle matroska container
+ if (string.Equals(format, "matroska", StringComparison.OrdinalIgnoreCase))
+ {
+ return "mkv";
+ }
return format;
}
@@ -560,8 +569,8 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- if (string.IsNullOrWhiteSpace(name) ||
- string.IsNullOrWhiteSpace(value))
+ if (string.IsNullOrWhiteSpace(name)
+ || string.IsNullOrWhiteSpace(value))
{
return null;
}
@@ -598,7 +607,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <returns>MediaAttachments.</returns>
private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
{
- if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)
+ if (streamInfo.CodecType != CodecType.Attachment
&& streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1)
{
return null;
@@ -626,17 +635,6 @@ namespace MediaBrowser.MediaEncoding.Probing
}
/// <summary>
- /// Determines whether a stream code time base is double the frame rate.
- /// </summary>
- /// <param name="averageFrameRate">average frame rate.</param>
- /// <param name="codecTimeBase">codec time base string.</param>
- /// <returns>true if the codec time base is double the frame rate.</returns>
- internal static bool IsCodecTimeBaseDoubleTheFrameRate(float? averageFrameRate, string codecTimeBase)
- {
- return MathF.Abs(((averageFrameRate ?? 0) * (GetFrameRate(codecTimeBase) ?? 0)) - 0.5f) <= float.Epsilon;
- }
-
- /// <summary>
/// Converts ffprobe stream info to our MediaStream class.
/// </summary>
/// <param name="isAudio">if set to <c>true</c> [is info].</param>
@@ -661,20 +659,10 @@ namespace MediaBrowser.MediaEncoding.Probing
PixelFormat = streamInfo.PixelFormat,
NalLengthSize = streamInfo.NalLengthSize,
TimeBase = streamInfo.TimeBase,
- CodecTimeBase = streamInfo.CodecTimeBase
+ CodecTimeBase = streamInfo.CodecTimeBase,
+ IsAVC = streamInfo.IsAvc
};
- if (string.Equals(streamInfo.IsAvc, "true", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(streamInfo.IsAvc, "1", StringComparison.OrdinalIgnoreCase))
- {
- stream.IsAVC = true;
- }
- else if (string.Equals(streamInfo.IsAvc, "false", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(streamInfo.IsAvc, "0", StringComparison.OrdinalIgnoreCase))
- {
- stream.IsAVC = false;
- }
-
// Filter out junk
if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase))
{
@@ -688,18 +676,15 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.Title = GetDictionaryValue(streamInfo.Tags, "title");
}
- if (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase))
+ if (streamInfo.CodecType == CodecType.Audio)
{
stream.Type = MediaStreamType.Audio;
stream.Channels = streamInfo.Channels;
- if (!string.IsNullOrEmpty(streamInfo.SampleRate))
+ if (int.TryParse(streamInfo.SampleRate, CultureInfo.InvariantCulture, out var sampleRate))
{
- if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
- {
- stream.SampleRate = value;
- }
+ stream.SampleRate = sampleRate;
}
stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout);
@@ -723,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
}
- else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase))
+ else if (streamInfo.CodecType == CodecType.Subtitle)
{
stream.Type = MediaStreamType.Subtitle;
stream.Codec = NormalizeSubtitleCodec(stream.Codec);
@@ -743,27 +728,14 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
}
- else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
+ else if (streamInfo.CodecType == CodecType.Video)
{
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
- bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+ stream.IsInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
- // Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe,
- // so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported
- // frame rate to determine if the file is interlaced
-
- bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
- && string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
- && IsCodecTimeBaseDoubleTheFrameRate(stream.AverageFrameRate, stream.CodecTimeBase);
-
- if (videoInterlaced || h264MbaffCoded)
- {
- stream.IsInterlaced = true;
- }
-
if (isAudio
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
@@ -877,35 +849,30 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
}
- else if (string.Equals(streamInfo.CodecType, "data", StringComparison.OrdinalIgnoreCase))
+ else if (streamInfo.CodecType == CodecType.Data)
{
stream.Type = MediaStreamType.Data;
}
else
{
- _logger.LogError("Codec Type {CodecType} unknown. The stream (index: {Index}) will be ignored. Warning: Subsequential streams will have a wrong stream specifier!", streamInfo.CodecType, streamInfo.Index);
return null;
}
// Get stream bitrate
var bitrate = 0;
- if (!string.IsNullOrEmpty(streamInfo.BitRate))
+ if (int.TryParse(streamInfo.BitRate, CultureInfo.InvariantCulture, out var value))
{
- if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
- {
- bitrate = value;
- }
+ bitrate = value;
}
// The bitrate info of FLAC musics and some videos is included in formatInfo.
if (bitrate == 0
&& formatInfo is not null
- && !string.IsNullOrEmpty(formatInfo.BitRate)
&& (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio)))
{
// If the stream info doesn't have a bitrate get the value from the media format info
- if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
+ if (int.TryParse(formatInfo.BitRate, CultureInfo.InvariantCulture, out value))
{
bitrate = value;
}
@@ -918,29 +885,26 @@ namespace MediaBrowser.MediaEncoding.Probing
// Extract bitrate info from tag "BPS" if possible.
if (!stream.BitRate.HasValue
- && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
- || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
+ && (streamInfo.CodecType == CodecType.Audio
+ || streamInfo.CodecType == CodecType.Video))
{
var bps = GetBPSFromTags(streamInfo);
if (bps > 0)
{
stream.BitRate = bps;
}
- }
-
- // Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
- if (!stream.BitRate.HasValue
- && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
- || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
- {
- var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
- var bytes = GetNumberOfBytesFromTags(streamInfo);
- if (durationInSeconds is not null && bytes is not null)
+ else
{
- var bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
- if (bps > 0)
+ // Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
+ var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
+ var bytes = GetNumberOfBytesFromTags(streamInfo);
+ if (durationInSeconds is not null && bytes is not null)
{
- stream.BitRate = bps;
+ bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
+ if (bps > 0)
+ {
+ stream.BitRate = bps;
+ }
}
}
}
@@ -971,12 +935,8 @@ namespace MediaBrowser.MediaEncoding.Probing
private void NormalizeStreamTitle(MediaStream stream)
{
- if (string.Equals(stream.Title, "cc", StringComparison.OrdinalIgnoreCase))
- {
- stream.Title = null;
- }
-
- if (stream.Type == MediaStreamType.EmbeddedImage)
+ if (string.Equals(stream.Title, "cc", StringComparison.OrdinalIgnoreCase)
+ || stream.Type == MediaStreamType.EmbeddedImage)
{
stream.Title = null;
}
@@ -1007,7 +967,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return null;
}
- return input.Split('(').FirstOrDefault();
+ return input.AsSpan().LeftPart('(').ToString();
}
private string GetAspectRatio(MediaStreamInfo info)
@@ -1015,11 +975,11 @@ namespace MediaBrowser.MediaEncoding.Probing
var original = info.DisplayAspectRatio;
var parts = (original ?? string.Empty).Split(':');
- if (!(parts.Length == 2 &&
- int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) &&
- int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) &&
- width > 0 &&
- height > 0))
+ if (!(parts.Length == 2
+ && int.TryParse(parts[0], CultureInfo.InvariantCulture, out var width)
+ && int.TryParse(parts[1], CultureInfo.InvariantCulture, out var height)
+ && width > 0
+ && height > 0))
{
width = info.Width;
height = info.Height;
@@ -1100,12 +1060,6 @@ namespace MediaBrowser.MediaEncoding.Probing
int index = value.IndexOf('/');
if (index == -1)
{
- // REVIEW: is this branch actually required? (i.e. does ffprobe ever output something other than a fraction?)
- if (float.TryParse(value, NumberStyles.AllowThousands | NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
-
return null;
}
@@ -1121,7 +1075,7 @@ namespace MediaBrowser.MediaEncoding.Probing
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
{
// Get the first info stream
- var stream = result.Streams?.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase));
+ var stream = result.Streams?.FirstOrDefault(s => s.CodecType == CodecType.Audio);
if (stream is null)
{
return;
@@ -1151,8 +1105,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS");
- if (!string.IsNullOrEmpty(bps)
- && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
+ if (int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
{
return parsedBps;
}
@@ -1168,7 +1121,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
- if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration))
+ if (TimeSpan.TryParse(duration, out var parsedDuration))
{
return parsedDuration.TotalSeconds;
}
@@ -1185,8 +1138,7 @@ namespace MediaBrowser.MediaEncoding.Probing
var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng")
?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES");
- if (!string.IsNullOrEmpty(numberOfBytes)
- && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
+ if (long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
{
return parsedBytes;
}
@@ -1478,7 +1430,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
var disc = tags.GetValueOrDefault(tagName);
- if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
+ if (int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
{
return discNum;
}
@@ -1498,7 +1450,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// Limit accuracy to milliseconds to match xml saving
var secondsString = chapter.StartTime;
- if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
+ if (double.TryParse(secondsString, CultureInfo.InvariantCulture, out var seconds))
{
var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs
index 695267d46..c6580598b 100644
--- a/MediaBrowser.Model/Branding/BrandingOptions.cs
+++ b/MediaBrowser.Model/Branding/BrandingOptions.cs
@@ -22,7 +22,7 @@ public class BrandingOptions
/// <summary>
/// Gets or sets a value indicating whether to enable the splashscreen.
/// </summary>
- public bool SplashscreenEnabled { get; set; } = true;
+ public bool SplashscreenEnabled { get; set; } = false;
/// <summary>
/// Gets or sets the splashscreen location on disk.
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 0ff95a2e1..f348d8417 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -39,7 +39,8 @@ public class EncodingOptions
DeinterlaceMethod = "yadif";
EnableDecodingColorDepth10Hevc = true;
EnableDecodingColorDepth10Vp9 = true;
- EnableEnhancedNvdecDecoder = false;
+ // Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping.
+ EnableEnhancedNvdecDecoder = true;
PreferSystemNativeHwDecoder = true;
EnableIntelLowPowerH264HwEncoder = false;
EnableIntelLowPowerHevcHwEncoder = false;
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index d3e042aba..7cb07a2f7 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -243,21 +243,21 @@ namespace MediaBrowser.Model.Configuration
public bool AllowClientLogUpload { get; set; } = true;
/// <summary>
- /// Gets or sets the dummy chapters duration in seconds.
+ /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
/// </summary>
/// <value>The dummy chapters duration.</value>
- public int DummyChapterDuration { get; set; } = 300;
-
- /// <summary>
- /// Gets or sets the dummy chapter count.
- /// </summary>
- /// <value>The dummy chapter count.</value>
- public int DummyChapterCount { get; set; } = 100;
+ public int DummyChapterDuration { get; set; } = 0;
/// <summary>
/// Gets or sets the chapter image resolution.
/// </summary>
/// <value>The chapter image resolution.</value>
public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+
+ /// <summary>
+ /// Gets or sets the limit for parallel image encoding.
+ /// </summary>
+ /// <value>The limit for parallel image encoding.</value>
+ public int ParallelImageEncodingLimit { get; set; } = 0;
}
}
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index 573422416..f5e1a3c49 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -136,12 +136,26 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
- if (int.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected))
+ var conditionType = condition.Condition;
+ if (condition.Condition == ProfileConditionType.EqualsAny)
{
- switch (condition.Condition)
+ foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
+ {
+ if (int.TryParse(singleConditionString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int conditionValue)
+ && conditionValue.Equals(currentValue))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (int.TryParse(condition.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var expected))
+ {
+ switch (conditionType)
{
case ProfileConditionType.Equals:
- case ProfileConditionType.EqualsAny:
return currentValue.Value.Equals(expected);
case ProfileConditionType.GreaterThanEqual:
return currentValue.Value >= expected;
@@ -212,9 +226,24 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
- if (double.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected))
+ var conditionType = condition.Condition;
+ if (condition.Condition == ProfileConditionType.EqualsAny)
{
- switch (condition.Condition)
+ foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
+ {
+ if (double.TryParse(singleConditionString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double conditionValue)
+ && conditionValue.Equals(currentValue))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (double.TryParse(condition.Value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var expected))
+ {
+ switch (conditionType)
{
case ProfileConditionType.Equals:
return currentValue.Value.Equals(expected);
diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs
index 927df8e4e..978004268 100644
--- a/MediaBrowser.Model/Dlna/ContainerProfile.cs
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
- public ProfileCondition[]? Conditions { get; set; } = Array.Empty<ProfileCondition>();
+ public ProfileCondition[] Conditions { get; set; } = Array.Empty<ProfileCondition>();
[XmlAttribute("container")]
public string Container { get; set; } = string.Empty;
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index 03c3a7265..f68235d86 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -18,17 +18,17 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
- public bool SupportsContainer(string container)
+ public bool SupportsContainer(string? container)
{
return ContainerProfile.ContainsContainer(Container, container);
}
- public bool SupportsVideoCodec(string codec)
+ public bool SupportsVideoCodec(string? codec)
{
return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec);
}
- public bool SupportsAudioCodec(string codec)
+ public bool SupportsAudioCodec(string? codec)
{
return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec);
}
diff --git a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
index a70ce44cc..d7397399d 100644
--- a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
+++ b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
@@ -10,22 +10,4 @@ namespace MediaBrowser.Model.Dlna
bool CanExtractSubtitles(string codec);
}
-
- public class FullTranscoderSupport : ITranscoderSupport
- {
- public bool CanEncodeToAudioCodec(string codec)
- {
- return true;
- }
-
- public bool CanEncodeToSubtitleCodec(string codec)
- {
- return true;
- }
-
- public bool CanExtractSubtitles(string codec)
- {
- return true;
- }
- }
}
diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs
index 29aecf97f..7ec0dd473 100644
--- a/MediaBrowser.Model/Dlna/MediaOptions.cs
+++ b/MediaBrowser.Model/Dlna/MediaOptions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using MediaBrowser.Model.Dto;
@@ -59,22 +57,22 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// Gets or sets the media sources.
/// </summary>
- public MediaSourceInfo[] MediaSources { get; set; }
+ public MediaSourceInfo[] MediaSources { get; set; } = Array.Empty<MediaSourceInfo>();
/// <summary>
/// Gets or sets the device profile.
/// </summary>
- public DeviceProfile Profile { get; set; }
+ required public DeviceProfile Profile { get; set; }
/// <summary>
/// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
/// </summary>
- public string MediaSourceId { get; set; }
+ public string? MediaSourceId { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
- public string DeviceId { get; set; }
+ public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets an override of supported number of audio channels
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
index 7fef16e53..7df53c6d1 100644
--- a/MediaBrowser.Model/Dlna/SortCriteria.cs
+++ b/MediaBrowser.Model/Dlna/SortCriteria.cs
@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Dlna
{
public SortCriteria(string sortOrder)
{
- if (!string.IsNullOrEmpty(sortOrder) && Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue))
+ if (Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue))
{
SortOrder = sortOrderValue;
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index bb41c9979..84c8c012c 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -38,53 +36,36 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
- /// </summary>
- /// <param name="logger">The <see cref="ILogger"/> object.</param>
- public StreamBuilder(ILogger<StreamBuilder> logger)
- : this(new FullTranscoderSupport(), logger)
- {
- }
-
- /// <summary>
/// Gets the optimal audio stream.
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns>
- public StreamInfo GetOptimalAudioStream(MediaOptions options)
+ public StreamInfo? GetOptimalAudioStream(MediaOptions options)
{
ValidateMediaOptions(options, false);
- var mediaSources = new List<MediaSourceInfo>();
+ var streams = new List<StreamInfo>();
foreach (var mediaSource in options.MediaSources)
{
- if (string.IsNullOrEmpty(options.MediaSourceId) ||
- string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+ if (!(string.IsNullOrEmpty(options.MediaSourceId)
+ || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)))
{
- mediaSources.Add(mediaSource);
+ continue;
}
- }
- var streams = new List<StreamInfo>();
- foreach (var mediaSourceInfo in mediaSources)
- {
- StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options);
+ StreamInfo? streamInfo = GetOptimalAudioStream(mediaSource, options);
if (streamInfo is not null)
{
+ streamInfo.DeviceId = options.DeviceId;
+ streamInfo.DeviceProfileId = options.Profile.Id;
streams.Add(streamInfo);
}
}
- foreach (var stream in streams)
- {
- stream.DeviceId = options.DeviceId;
- stream.DeviceProfileId = options.Profile.Id;
- }
-
return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
}
- private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
+ private StreamInfo? GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
{
var playlistItem = new StreamInfo
{
@@ -118,7 +99,7 @@ namespace MediaBrowser.Model.Dlna
var transcodeReasons = directPlayInfo.TranscodeReasons;
var inputAudioChannels = audioStream?.Channels;
- var inputAudioBitrate = audioStream?.BitDepth;
+ var inputAudioBitrate = audioStream?.BitRate;
var inputAudioSampleRate = audioStream?.SampleRate;
var inputAudioBitDepth = audioStream?.BitDepth;
@@ -138,12 +119,12 @@ namespace MediaBrowser.Model.Dlna
}
}
- TranscodingProfile transcodingProfile = null;
+ TranscodingProfile? transcodingProfile = null;
foreach (var tcProfile in options.Profile.TranscodingProfiles)
{
if (tcProfile.Type == playlistItem.MediaType
&& tcProfile.Context == options.Context
- && _transcoderSupport.CanEncodeToAudioCodec(transcodingProfile.AudioCodec ?? tcProfile.Container))
+ && _transcoderSupport.CanEncodeToAudioCodec(tcProfile.AudioCodec ?? tcProfile.Container))
{
transcodingProfile = tcProfile;
break;
@@ -190,15 +171,15 @@ namespace MediaBrowser.Model.Dlna
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns>
- public StreamInfo GetOptimalVideoStream(MediaOptions options)
+ public StreamInfo? GetOptimalVideoStream(MediaOptions options)
{
ValidateMediaOptions(options, true);
var mediaSources = new List<MediaSourceInfo>();
foreach (var mediaSourceInfo in options.MediaSources)
{
- if (string.IsNullOrEmpty(options.MediaSourceId) ||
- string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(options.MediaSourceId)
+ || string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{
mediaSources.Add(mediaSourceInfo);
}
@@ -223,7 +204,7 @@ namespace MediaBrowser.Model.Dlna
return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
}
- private static StreamInfo GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
+ private static StreamInfo? GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
=> SortMediaSources(streams, maxBitrate).FirstOrDefault();
private static IOrderedEnumerable<StreamInfo> SortMediaSources(List<StreamInfo> streams, long maxBitrate)
@@ -366,7 +347,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="type">The <see cref="DlnaProfileType"/>.</param>
/// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
/// <returns>The the normalized input container.</returns>
- public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
+ public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null)
{
if (string.IsNullOrEmpty(inputContainer))
{
@@ -394,7 +375,7 @@ namespace MediaBrowser.Model.Dlna
return formats[0];
}
- private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
+ private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
{
var directPlayProfile = options.Profile.DirectPlayProfiles
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
@@ -410,7 +391,6 @@ namespace MediaBrowser.Model.Dlna
return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
- var playMethods = new List<PlayMethod>();
TranscodeReason transcodeReasons = 0;
// The profile describes what the device supports
@@ -449,7 +429,7 @@ namespace MediaBrowser.Model.Dlna
return (directPlayProfile, null, transcodeReasons);
}
- private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
+ private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream? videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
{
var mediaType = videoStream is null ? DlnaProfileType.Audio : DlnaProfileType.Video;
@@ -551,8 +531,7 @@ namespace MediaBrowser.Model.Dlna
}
playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
- if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels)
- && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels))
+ if (int.TryParse(transcodingProfile.MaxAudioChannels, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels))
{
playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
}
@@ -576,7 +555,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
+ private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile)
{
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
var protocol = "http";
@@ -588,7 +567,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.SubProtocol = protocol;
playlistItem.VideoCodecs = new[] { item.VideoStream.Codec };
- playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
+ playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
}
private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
@@ -635,6 +614,12 @@ namespace MediaBrowser.Model.Dlna
var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded);
TranscodeReason transcodeReasons = 0;
+ // Force transcode or remux for BD/DVD folders
+ if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay)
+ {
+ isEligibleForDirectPlay = false;
+ }
+
if (bitrateLimitExceeded)
{
transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
@@ -647,7 +632,7 @@ namespace MediaBrowser.Model.Dlna
isEligibleForDirectPlay,
isEligibleForDirectStream);
- DirectPlayProfile directPlayProfile = null;
+ DirectPlayProfile? directPlayProfile = null;
if (isEligibleForDirectPlay || isEligibleForDirectStream)
{
// See if it can be direct played
@@ -678,16 +663,16 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioStreamIndex = audioStream?.Index;
if (audioStream is not null)
{
- playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
+ playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
}
SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
- BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec);
+ BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile?.Container, directPlayProfile?.VideoCodec, directPlayProfile?.AudioCodec);
}
if (subtitleStream is not null)
{
- var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null);
+ var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile?.Container, null);
playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
playlistItem.SubtitleFormat = subtitleProfile.Format;
@@ -749,7 +734,14 @@ namespace MediaBrowser.Model.Dlna
return playlistItem;
}
- private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
+ private TranscodingProfile? GetVideoTranscodeProfile(
+ MediaSourceInfo item,
+ MediaOptions options,
+ MediaStream? videoStream,
+ MediaStream? audioStream,
+ IEnumerable<MediaStream> candidateAudioStreams,
+ MediaStream? subtitleStream,
+ StreamInfo playlistItem)
{
if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{
@@ -796,7 +788,16 @@ namespace MediaBrowser.Model.Dlna
return transcodingProfiles.FirstOrDefault();
}
- private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
+ private void BuildStreamVideoItem(
+ StreamInfo playlistItem,
+ MediaOptions options,
+ MediaSourceInfo item,
+ MediaStream? videoStream,
+ MediaStream? audioStream,
+ IEnumerable<MediaStream> candidateAudioStreams,
+ string? container,
+ string? videoCodec,
+ string? audioCodec)
{
// Prefer matching video codecs
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
@@ -863,12 +864,12 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth = videoStream?.BitDepth;
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
- string videoProfile = videoStream?.Profile;
- string videoRangeType = videoStream?.VideoRangeType;
+ string? videoProfile = videoStream?.Profile;
+ string? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
- string videoCodecTag = videoStream?.CodecTag;
+ string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
@@ -904,11 +905,11 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
- int? inputAudioBitrate = audioStream is null ? null : audioStream.BitRate;
- int? audioChannels = audioStream is null ? null : audioStream.Channels;
- string audioProfile = audioStream is null ? null : audioStream.Profile;
- int? inputAudioSampleRate = audioStream is null ? null : audioStream.SampleRate;
- int? inputAudioBitDepth = audioStream is null ? null : audioStream.BitDepth;
+ int? inputAudioBitrate = audioStream?.BitRate;
+ int? audioChannels = audioStream?.Channels;
+ string? audioProfile = audioStream?.Profile;
+ int? inputAudioSampleRate = audioStream?.SampleRate;
+ int? inputAudioBitDepth = audioStream?.BitDepth;
var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
@@ -956,7 +957,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem?.TranscodeReasons);
}
- private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels)
+ private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
{
if (!string.IsNullOrEmpty(audioCodec))
{
@@ -989,9 +990,9 @@ namespace MediaBrowser.Model.Dlna
return 192000;
}
- private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item)
+ private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item)
{
- string targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+ string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
@@ -1082,13 +1083,13 @@ namespace MediaBrowser.Model.Dlna
return 7168000;
}
- private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
+ private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
MediaOptions options,
MediaSourceInfo mediaSource,
- MediaStream videoStream,
- MediaStream audioStream,
+ MediaStream? videoStream,
+ MediaStream? audioStream,
ICollection<MediaStream> candidateAudioStreams,
- MediaStream subtitleStream,
+ MediaStream? subtitleStream,
bool isEligibleForDirectPlay,
bool isEligibleForDirectStream)
{
@@ -1111,12 +1112,12 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth = videoStream?.BitDepth;
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
- string videoProfile = videoStream?.Profile;
- string videoRangeType = videoStream?.VideoRangeType;
+ string? videoProfile = videoStream?.Profile;
+ string? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
- string videoCodecTag = videoStream?.CodecTag;
+ string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
@@ -1174,7 +1175,6 @@ namespace MediaBrowser.Model.Dlna
var reason = a & flag;
if (reason != 0)
{
- a = reason;
return index;
}
@@ -1184,6 +1184,8 @@ namespace MediaBrowser.Model.Dlna
return index;
};
+ var containerSupported = false;
+
// Check DirectPlay profiles to see if it can be direct played
var analyzedProfiles = profile.DirectPlayProfiles
.Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video)
@@ -1197,16 +1199,20 @@ namespace MediaBrowser.Model.Dlna
{
directPlayProfileReasons |= TranscodeReason.ContainerNotSupported;
}
+ else
+ {
+ containerSupported = true;
+ }
// Check video codec
- string videoCodec = videoStream?.Codec;
+ string? videoCodec = videoStream?.Codec;
if (!directPlayProfile.SupportsVideoCodec(videoCodec))
{
directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported;
}
// Check audio codec
- MediaStream selectedAudioStream = null;
+ MediaStream? selectedAudioStream = null;
if (candidateAudioStreams.Any())
{
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
@@ -1239,7 +1245,7 @@ namespace MediaBrowser.Model.Dlna
{
playMethod = PlayMethod.DirectPlay;
}
- else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectStream && directPlayProfile is not null)
+ else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectStream)
{
playMethod = PlayMethod.DirectStream;
}
@@ -1261,7 +1267,10 @@ namespace MediaBrowser.Model.Dlna
return profileMatch;
}
- var failureReasons = analyzedProfiles[false].Select(analysis => analysis.Result).FirstOrDefault().TranscodeReason;
+ var failureReasons = analyzedProfiles[false]
+ .Select(analysis => analysis.Result)
+ .Where(result => !containerSupported || (result.TranscodeReason & TranscodeReason.ContainerNotSupported) == 0)
+ .FirstOrDefault().TranscodeReason;
if (failureReasons == 0)
{
failureReasons = TranscodeReason.DirectPlayError;
@@ -1324,8 +1333,8 @@ namespace MediaBrowser.Model.Dlna
SubtitleProfile[] subtitleProfiles,
PlayMethod playMethod,
ITranscoderSupport transcoderSupport,
- string outputContainer,
- string transcodingSubProtocol)
+ string? outputContainer,
+ string? transcodingSubProtocol)
{
if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)))
{
@@ -1398,7 +1407,7 @@ namespace MediaBrowser.Model.Dlna
};
}
- private static bool IsSubtitleEmbedSupported(string transcodingContainer)
+ private static bool IsSubtitleEmbedSupported(string? transcodingContainer)
{
if (!string.IsNullOrEmpty(transcodingContainer))
{
@@ -1420,7 +1429,7 @@ namespace MediaBrowser.Model.Dlna
return false;
}
- private static SubtitleProfile GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
+ private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
{
foreach (var profile in subtitleProfiles)
{
@@ -1553,7 +1562,7 @@ namespace MediaBrowser.Model.Dlna
private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio(
IEnumerable<CodecProfile> codecProfiles,
string container,
- string codec,
+ string? codec,
int? audioChannels,
int? audioBitrate,
int? audioSampleRate,
@@ -1573,7 +1582,7 @@ namespace MediaBrowser.Model.Dlna
return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth));
}
- private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions)
+ private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string? qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions)
{
foreach (ProfileCondition condition in conditions)
{
@@ -1599,7 +1608,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1625,7 +1634,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1661,7 +1670,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1785,7 +1794,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1821,7 +1830,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1911,7 +1920,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1937,7 +1946,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1963,7 +1972,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (float.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -1989,7 +1998,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -2015,7 +2024,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
{
if (condition.Condition == ProfileConditionType.Equals)
{
@@ -2049,7 +2058,7 @@ namespace MediaBrowser.Model.Dlna
}
// Check audio codec
- string audioCodec = audioStream?.Codec;
+ string? audioCodec = audioStream?.Codec;
if (!profile.SupportsAudioCodec(audioCodec))
{
return false;
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 3b5509907..886b64a24 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -107,9 +107,8 @@ namespace MediaBrowser.Model.Dlna
public string MediaSourceId => MediaSource?.Id;
- public bool IsDirectStream =>
- PlayMethod == PlayMethod.DirectStream ||
- PlayMethod == PlayMethod.DirectPlay;
+ public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
+ && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
/// <summary>
/// Gets the audio stream that will be used.
@@ -922,12 +921,8 @@ namespace MediaBrowser.Model.Dlna
public int? GetTargetVideoBitDepth(string codec)
{
var value = GetOption(codec, "videobitdepth");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
- if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
@@ -938,12 +933,8 @@ namespace MediaBrowser.Model.Dlna
public int? GetTargetAudioBitDepth(string codec)
{
var value = GetOption(codec, "audiobitdepth");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
- if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
@@ -954,12 +945,8 @@ namespace MediaBrowser.Model.Dlna
public double? GetTargetVideoLevel(string codec)
{
var value = GetOption(codec, "level");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
- if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ if (double.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
@@ -970,12 +957,8 @@ namespace MediaBrowser.Model.Dlna
public int? GetTargetRefFrames(string codec)
{
var value = GetOption(codec, "maxrefframes");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
return result;
}
diff --git a/MediaBrowser.Model/Dto/ImageOptions.cs b/MediaBrowser.Model/Dto/ImageOptions.cs
deleted file mode 100644
index 3f4405f1e..000000000
--- a/MediaBrowser.Model/Dto/ImageOptions.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-#nullable disable
-using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Model.Dto
-{
- /// <summary>
- /// Class ImageOptions.
- /// </summary>
- public class ImageOptions
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="ImageOptions" /> class.
- /// </summary>
- public ImageOptions()
- {
- EnableImageEnhancers = true;
- }
-
- /// <summary>
- /// Gets or sets the type of the image.
- /// </summary>
- /// <value>The type of the image.</value>
- public ImageType ImageType { get; set; }
-
- /// <summary>
- /// Gets or sets the index of the image.
- /// </summary>
- /// <value>The index of the image.</value>
- public int? ImageIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the width.
- /// </summary>
- /// <value>The width.</value>
- public int? Width { get; set; }
-
- /// <summary>
- /// Gets or sets the height.
- /// </summary>
- /// <value>The height.</value>
- public int? Height { get; set; }
-
- /// <summary>
- /// Gets or sets the width of the max.
- /// </summary>
- /// <value>The width of the max.</value>
- public int? MaxWidth { get; set; }
-
- /// <summary>
- /// Gets or sets the height of the max.
- /// </summary>
- /// <value>The height of the max.</value>
- public int? MaxHeight { get; set; }
-
- /// <summary>
- /// Gets or sets the quality.
- /// </summary>
- /// <value>The quality.</value>
- public int? Quality { get; set; }
-
- /// <summary>
- /// Gets or sets the image tag.
- /// If set this will result in strong, unconditional response caching.
- /// </summary>
- /// <value>The hash.</value>
- public string Tag { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [crop whitespace].
- /// </summary>
- /// <value><c>null</c> if [crop whitespace] contains no value, <c>true</c> if [crop whitespace]; otherwise, <c>false</c>.</value>
- public bool? CropWhitespace { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [enable image enhancers].
- /// </summary>
- /// <value><c>true</c> if [enable image enhancers]; otherwise, <c>false</c>.</value>
- public bool EnableImageEnhancers { get; set; }
-
- /// <summary>
- /// Gets or sets the format.
- /// </summary>
- /// <value>The format.</value>
- public ImageFormat? Format { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [add played indicator].
- /// </summary>
- /// <value><c>true</c> if [add played indicator]; otherwise, <c>false</c>.</value>
- public bool AddPlayedIndicator { get; set; }
-
- /// <summary>
- /// Gets or sets the percent played.
- /// </summary>
- /// <value>The percent played.</value>
- public int? PercentPlayed { get; set; }
-
- /// <summary>
- /// Gets or sets the un played count.
- /// </summary>
- /// <value>The un played count.</value>
- public int? UnPlayedCount { get; set; }
-
- /// <summary>
- /// Gets or sets the color of the background.
- /// </summary>
- /// <value>The color of the background.</value>
- public string BackgroundColor { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs
index 17b2868a3..c92640818 100644
--- a/MediaBrowser.Model/Entities/ParentalRating.cs
+++ b/MediaBrowser.Model/Entities/ParentalRating.cs
@@ -12,7 +12,7 @@ namespace MediaBrowser.Model.Entities
{
}
- public ParentalRating(string name, int value)
+ public ParentalRating(string name, int? value)
{
Name = name;
Value = value;
@@ -28,6 +28,6 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets the value.
/// </summary>
/// <value>The value.</value>
- public int Value { get; set; }
+ public int? Value { get; set; }
}
}
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index e00157dce..02a29e7fa 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -30,8 +30,9 @@ namespace MediaBrowser.Model.Globalization
/// Gets the rating level.
/// </summary>
/// <param name="rating">The rating.</param>
+ /// <param name="countryCode">The optional two letter ISO language string.</param>
/// <returns><see cref="int" /> or <c>null</c>.</returns>
- int? GetRatingLevel(string rating);
+ int? GetRatingLevel(string rating, string? countryCode = null);
/// <summary>
/// Gets the localized string.
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 284e89f1c..9a5804485 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -33,14 +33,14 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
- <PackageReference Include="MimeTypes" Version="2.4.0">
+ <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
+ <PackageReference Include="MimeTypes">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="System.Globalization" Version="4.3.0" />
- <PackageReference Include="System.Text.Json" Version="7.0.1" />
+ <PackageReference Include="System.Globalization" />
+ <PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
@@ -49,13 +49,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Jellyfin.Data/Jellyfin.Data.csproj" />
diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
index 83f982a5c..d546ffccd 100644
--- a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
+++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
@@ -1,39 +1,41 @@
#nullable disable
-#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Model.MediaInfo
+namespace MediaBrowser.Model.MediaInfo;
+
+/// <summary>
+/// Represents the result of BDInfo output.
+/// </summary>
+public class BlurayDiscInfo
{
/// <summary>
- /// Represents the result of BDInfo output.
+ /// Gets or sets the media streams.
/// </summary>
- public class BlurayDiscInfo
- {
- /// <summary>
- /// Gets or sets the media streams.
- /// </summary>
- /// <value>The media streams.</value>
- public MediaStream[] MediaStreams { get; set; }
+ /// <value>The media streams.</value>
+ public MediaStream[] MediaStreams { get; set; }
- /// <summary>
- /// Gets or sets the run time ticks.
- /// </summary>
- /// <value>The run time ticks.</value>
- public long? RunTimeTicks { get; set; }
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ public long? RunTimeTicks { get; set; }
- /// <summary>
- /// Gets or sets the files.
- /// </summary>
- /// <value>The files.</value>
- public string[] Files { get; set; }
+ /// <summary>
+ /// Gets or sets the files.
+ /// </summary>
+ /// <value>The files.</value>
+ public string[] Files { get; set; }
- public string PlaylistName { get; set; }
+ /// <summary>
+ /// Gets or sets the playlist name.
+ /// </summary>
+ /// <value>The playlist name.</value>
+ public string PlaylistName { get; set; }
- /// <summary>
- /// Gets or sets the chapters.
- /// </summary>
- /// <value>The chapters.</value>
- public double[] Chapters { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the chapters.
+ /// </summary>
+ /// <value>The chapters.</value>
+ public double[] Chapters { get; set; }
}
diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
index 5b7d1d03c..d39725301 100644
--- a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
+++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
@@ -1,15 +1,14 @@
-namespace MediaBrowser.Model.MediaInfo
+namespace MediaBrowser.Model.MediaInfo;
+
+/// <summary>
+/// Interface IBlurayExaminer.
+/// </summary>
+public interface IBlurayExaminer
{
/// <summary>
- /// Interface IBlurayExaminer.
+ /// Gets the disc info.
/// </summary>
- public interface IBlurayExaminer
- {
- /// <summary>
- /// Gets the disc info.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>BlurayDiscInfo.</returns>
- BlurayDiscInfo GetDiscInfo(string path);
- }
+ /// <param name="path">The path.</param>
+ /// <returns>BlurayDiscInfo.</returns>
+ BlurayDiscInfo GetDiscInfo(string path);
}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 8157dc0c2..5a1871070 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Net
// Type image
{ "image/jpeg", ".jpg" },
+ { "image/tiff", ".tiff" },
{ "image/x-png", ".png" },
+ { "image/x-icon", ".ico" },
// Type text
{ "text/plain", ".txt" },
@@ -178,5 +180,8 @@ namespace MediaBrowser.Model.Net
var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
return string.IsNullOrEmpty(extension) ? null : "." + extension;
}
+
+ public static bool IsImage(ReadOnlySpan<char> mimeType)
+ => mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
}
}
diff --git a/MediaBrowser.Model/Notifications/NotificationLevel.cs b/MediaBrowser.Model/Notifications/NotificationLevel.cs
deleted file mode 100644
index 14fead3f0..000000000
--- a/MediaBrowser.Model/Notifications/NotificationLevel.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Notifications
-{
- public enum NotificationLevel
- {
- Normal = 0,
- Warning = 1,
- Error = 2
- }
-}
diff --git a/MediaBrowser.Model/Notifications/NotificationOption.cs b/MediaBrowser.Model/Notifications/NotificationOption.cs
deleted file mode 100644
index 58aecb3d3..000000000
--- a/MediaBrowser.Model/Notifications/NotificationOption.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-#pragma warning disable CA1819 // Properties should not return arrays
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Notifications
-{
- public class NotificationOption
- {
- public NotificationOption(string type)
- {
- Type = type;
- DisabledServices = Array.Empty<string>();
- DisabledMonitorUsers = Array.Empty<string>();
- SendToUsers = Array.Empty<string>();
- }
-
- public NotificationOption()
- {
- DisabledServices = Array.Empty<string>();
- DisabledMonitorUsers = Array.Empty<string>();
- SendToUsers = Array.Empty<string>();
- }
-
- public string? Type { get; set; }
-
- /// <summary>
- /// Gets or sets user Ids to not monitor (it's opt out).
- /// </summary>
- public string[] DisabledMonitorUsers { get; set; }
-
- /// <summary>
- /// Gets or sets user Ids to send to (if SendToUserMode == Custom).
- /// </summary>
- public string[] SendToUsers { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this <see cref="NotificationOption"/> is enabled.
- /// </summary>
- /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value>
- public bool Enabled { get; set; }
-
- /// <summary>
- /// Gets or sets the disabled services.
- /// </summary>
- /// <value>The disabled services.</value>
- public string[] DisabledServices { get; set; }
-
- /// <summary>
- /// Gets or sets the send to user mode.
- /// </summary>
- /// <value>The send to user mode.</value>
- public SendToUserType SendToUserMode { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs
deleted file mode 100644
index 804f51e16..000000000
--- a/MediaBrowser.Model/Notifications/NotificationOptions.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
-
-namespace MediaBrowser.Model.Notifications
-{
- public class NotificationOptions
- {
- public NotificationOptions()
- {
- Options = new[]
- {
- new NotificationOption(NotificationType.TaskFailed.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.ServerRestartRequired.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.ApplicationUpdateAvailable.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.ApplicationUpdateInstalled.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.PluginUpdateInstalled.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.PluginUninstalled.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.InstallationFailed.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.PluginInstalled.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.PluginError.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- },
- new NotificationOption(NotificationType.UserLockedOut.ToString())
- {
- Enabled = true,
- SendToUserMode = SendToUserType.Admins
- }
- };
- }
-
- public NotificationOption[] Options { get; set; }
-
- public NotificationOption GetOptions(string type)
- {
- foreach (NotificationOption i in Options)
- {
- if (string.Equals(type, i.Type, StringComparison.OrdinalIgnoreCase))
- {
- return i;
- }
- }
-
- return null;
- }
-
- public bool IsEnabled(string type)
- {
- NotificationOption opt = GetOptions(type);
-
- return opt is not null && opt.Enabled;
- }
-
- public bool IsServiceEnabled(string service, string notificationType)
- {
- NotificationOption opt = GetOptions(notificationType);
-
- return opt is null
- || !opt.DisabledServices.Contains(service, StringComparison.OrdinalIgnoreCase);
- }
-
- public bool IsEnabledToMonitorUser(string type, Guid userId)
- {
- NotificationOption opt = GetOptions(type);
-
- return opt is not null
- && opt.Enabled
- && !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparison.OrdinalIgnoreCase);
- }
-
- public bool IsEnabledToSendToUser(string type, string userId, User user)
- {
- NotificationOption opt = GetOptions(type);
-
- if (opt is not null && opt.Enabled)
- {
- if (opt.SendToUserMode == SendToUserType.All)
- {
- return true;
- }
-
- if (opt.SendToUserMode == SendToUserType.Admins && user.HasPermission(PermissionKind.IsAdministrator))
- {
- return true;
- }
-
- return opt.SendToUsers.Contains(userId, StringComparison.OrdinalIgnoreCase);
- }
-
- return false;
- }
- }
-}
diff --git a/MediaBrowser.Model/Notifications/NotificationRequest.cs b/MediaBrowser.Model/Notifications/NotificationRequest.cs
deleted file mode 100644
index 622c50cd8..000000000
--- a/MediaBrowser.Model/Notifications/NotificationRequest.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Notifications
-{
- public class NotificationRequest
- {
- public NotificationRequest()
- {
- UserIds = Array.Empty<Guid>();
- Date = DateTime.UtcNow;
- }
-
- public string Name { get; set; }
-
- public string Description { get; set; }
-
- public string Url { get; set; }
-
- public NotificationLevel Level { get; set; }
-
- public Guid[] UserIds { get; set; }
-
- public DateTime Date { get; set; }
-
- /// <summary>
- /// Gets or sets the corresponding type name used in configuration. Not for display.
- /// </summary>
- public string NotificationType { get; set; }
-
- public SendToUserType? SendToUserMode { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs b/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs
deleted file mode 100644
index 402fbe81a..000000000
--- a/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Notifications
-{
- public class NotificationTypeInfo
- {
- public string Type { get; set; }
-
- public string Name { get; set; }
-
- public bool Enabled { get; set; }
-
- public string Category { get; set; }
-
- public bool IsBasedOnUserEvent { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Notifications/SendToUserType.cs b/MediaBrowser.Model/Notifications/SendToUserType.cs
deleted file mode 100644
index 65fc4e1ab..000000000
--- a/MediaBrowser.Model/Notifications/SendToUserType.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Notifications
-{
- public enum SendToUserType
- {
- All = 0,
- Admins = 1,
- Custom = 2
- }
-}
diff --git a/MediaBrowser.Model/System/OperatingSystemId.cs b/MediaBrowser.Model/System/OperatingSystemId.cs
deleted file mode 100644
index 2e417f6b5..000000000
--- a/MediaBrowser.Model/System/OperatingSystemId.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.System
-{
- public enum OperatingSystemId
- {
- Windows,
- Linux,
- Darwin,
- BSD
- }
-}
diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs
index 53030843a..31a895642 100644
--- a/MediaBrowser.Model/System/PublicSystemInfo.cs
+++ b/MediaBrowser.Model/System/PublicSystemInfo.cs
@@ -1,6 +1,8 @@
#nullable disable
#pragma warning disable CS1591
+using System;
+
namespace MediaBrowser.Model.System
{
public class PublicSystemInfo
@@ -32,7 +34,8 @@ namespace MediaBrowser.Model.System
/// Gets or sets the operating system.
/// </summary>
/// <value>The operating system.</value>
- public string OperatingSystem { get; set; }
+ [Obsolete("This is no longer set")]
+ public string OperatingSystem { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the id.
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index a82c1c8c0..bd0099af7 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -42,7 +42,8 @@ namespace MediaBrowser.Model.System
/// Gets or sets the display name of the operating system.
/// </summary>
/// <value>The display name of the operating system.</value>
- public string OperatingSystemDisplayName { get; set; }
+ [Obsolete("This is no longer set")]
+ public string OperatingSystemDisplayName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the package name.
@@ -79,8 +80,9 @@ namespace MediaBrowser.Model.System
/// <summary>
/// Gets or sets a value indicating whether this instance can self restart.
/// </summary>
- /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
- public bool CanSelfRestart { get; set; }
+ /// <value><c>true</c>.</value>
+ [Obsolete("This is always true")]
+ public bool CanSelfRestart { get; set; } = true;
public bool CanLaunchWebBrowser { get; set; }
@@ -136,6 +138,7 @@ namespace MediaBrowser.Model.System
[Obsolete("This isn't set correctly anymore")]
public FFmpegLocation EncoderLocation { get; set; }
- public Architecture SystemArchitecture { get; set; }
+ [Obsolete("This is no longer set")]
+ public Architecture SystemArchitecture { get; set; } = Architecture.X64;
}
}
diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs
index 13bebc479..5b55667e8 100644
--- a/MediaBrowser.Model/Tasks/ITaskManager.cs
+++ b/MediaBrowser.Model/Tasks/ITaskManager.cs
@@ -9,9 +9,9 @@ namespace MediaBrowser.Model.Tasks
{
public interface ITaskManager : IDisposable
{
- event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+ event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
- event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+ event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
/// <summary>
/// Gets the list of Scheduled Tasks.
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 3634d0705..80f5e2c37 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Users
public UserPolicy()
{
IsHidden = true;
+ EnableCollectionManagement = false;
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
@@ -35,6 +36,7 @@ namespace MediaBrowser.Model.Users
EnableSharedDeviceControl = true;
BlockedTags = Array.Empty<string>();
+ AllowedTags = Array.Empty<string>();
BlockUnratedItems = Array.Empty<UnratedItem>();
EnableUserPreferenceAccess = true;
@@ -44,6 +46,7 @@ namespace MediaBrowser.Model.Users
LoginAttemptsBeforeLockout = -1;
MaxActiveSessions = 0;
+ MaxParentalRating = null;
EnableAllChannels = true;
EnabledChannels = Array.Empty<Guid>();
@@ -73,6 +76,12 @@ namespace MediaBrowser.Model.Users
public bool IsHidden { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether this instance can manage collections.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool EnableCollectionManagement { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance is disabled.
/// </summary>
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
@@ -86,6 +95,8 @@ namespace MediaBrowser.Model.Users
public string[] BlockedTags { get; set; }
+ public string[] AllowedTags { get; set; }
+
public bool EnableUserPreferenceAccess { get; set; }
public AccessSchedule[] AccessSchedules { get; set; }
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index d621555f1..5d59c4663 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -313,7 +313,8 @@ namespace MediaBrowser.Providers.Manager
}
minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
- await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
+ var listWithNoLangFirst = list.OrderByDescending(i => string.IsNullOrEmpty(i.Language));
+ await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, listWithNoLangFirst, minWidth, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index ffae77200..0605b0bd7 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -151,7 +151,6 @@ namespace MediaBrowser.Providers.Manager
ApplySearchResult(id, refreshOptions.SearchResult);
}
- // await FindIdentities(id, cancellationToken).ConfigureAwait(false);
id.IsAutomated = refreshOptions.IsAutomated;
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
@@ -334,6 +333,12 @@ namespace MediaBrowser.Providers.Manager
updateType |= UpdateCumulativeRunTimeTicks(item, children);
updateType |= UpdateDateLastMediaAdded(item, children);
+ // don't update user-changeable metadata for locked items
+ if (item.IsLocked)
+ {
+ return updateType;
+ }
+
if (EnableUpdatingPremiereDateFromChildren)
{
updateType |= UpdatePremiereDate(item, children);
@@ -375,7 +380,7 @@ namespace MediaBrowser.Providers.Manager
if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks)
{
folder.RunTimeTicks = ticks;
- return ItemUpdateType.MetadataEdit;
+ return ItemUpdateType.MetadataImport;
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 0ce696edc..81ccd8653 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -284,12 +284,12 @@ namespace MediaBrowser.Providers.Manager
}
catch (OperationCanceledException)
{
- return new List<RemoteImageInfo>();
+ return Enumerable.Empty<RemoteImageInfo>();
}
catch (Exception ex)
{
_logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
- return new List<RemoteImageInfo>();
+ return Enumerable.Empty<RemoteImageInfo>();
}
}
@@ -910,19 +910,34 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/>
public void OnRefreshStart(BaseItem item)
{
- _logger.LogDebug("OnRefreshStart {Item}", item.Id.ToString("N", CultureInfo.InvariantCulture));
+ _logger.LogDebug("OnRefreshStart {Item:N}", item.Id);
_activeRefreshes[item.Id] = 0;
- RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
+ try
+ {
+ RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
+ }
+ catch (Exception ex)
+ {
+ // EventHandlers should never propagate exceptions, but we have little control over plugins...
+ _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshStarted));
+ }
}
/// <inheritdoc/>
public void OnRefreshComplete(BaseItem item)
{
- _logger.LogDebug("OnRefreshComplete {Item}", item.Id.ToString("N", CultureInfo.InvariantCulture));
-
- _activeRefreshes.Remove(item.Id, out _);
+ _logger.LogDebug("OnRefreshComplete {Item:N}", item.Id);
+ _activeRefreshes.TryRemove(item.Id, out _);
- RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
+ try
+ {
+ RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
+ }
+ catch (Exception ex)
+ {
+ // EventHandlers should never propagate exceptions, but we have little control over plugins...
+ _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshCompleted));
+ }
}
/// <inheritdoc/>
@@ -940,12 +955,12 @@ namespace MediaBrowser.Providers.Manager
public void OnRefreshProgress(BaseItem item, double progress)
{
var id = item.Id;
- _logger.LogDebug("OnRefreshProgress {Id} {Progress}", id.ToString("N", CultureInfo.InvariantCulture), progress);
+ _logger.LogDebug("OnRefreshProgress {Id:N} {Progress}", id, progress);
// TODO: Need to hunt down the conditions for this happening
_activeRefreshes.AddOrUpdate(
id,
- (_) => throw new InvalidOperationException(
+ _ => throw new InvalidOperationException(
string.Format(
CultureInfo.InvariantCulture,
"Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running",
@@ -953,7 +968,15 @@ namespace MediaBrowser.Providers.Manager
item.Id.ToString("N", CultureInfo.InvariantCulture))),
(_, _) => progress);
- RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
+ try
+ {
+ RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
+ }
+ catch (Exception ex)
+ {
+ // EventHandlers should never propagate exceptions, but we have little control over plugins...
+ _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshProgress));
+ }
}
/// <inheritdoc/>
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 97ad1ffbc..6a40833d7 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -8,7 +8,6 @@
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\DvdLib\DvdLib.csproj" />
</ItemGroup>
<ItemGroup>
@@ -16,15 +15,15 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="LrcParser" Version="2022.529.1" />
- <PackageReference Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
- <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
- <PackageReference Include="PlaylistsNET" Version="1.3.1" />
- <PackageReference Include="TagLibSharp" Version="2.3.0" />
- <PackageReference Include="TMDbLib" Version="2.0.0" />
+ <PackageReference Include="LrcParser" />
+ <PackageReference Include="MetaBrainz.MusicBrainz" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
+ <PackageReference Include="Microsoft.Extensions.Http" />
+ <PackageReference Include="Newtonsoft.Json" />
+ <PackageReference Include="PlaylistsNET" />
+ <PackageReference Include="TagLibSharp" />
+ <PackageReference Include="TMDbLib" />
</ItemGroup>
<PropertyGroup>
@@ -36,13 +35,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 74210b1f2..19b594c1c 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -105,7 +105,10 @@ namespace MediaBrowser.Providers.MediaInfo
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
audio.Size = mediaInfo.Size;
- FetchDataFromTags(audio);
+ if (!audio.IsLocked)
+ {
+ FetchDataFromTags(audio);
+ }
_itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken);
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 81434b862..213639371 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -9,7 +9,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using DvdLib.Ifo;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
@@ -90,36 +89,66 @@ namespace MediaBrowser.Providers.MediaInfo
if (!item.IsShortcut || options.EnableRemoteContentProbe)
{
- string[] streamFileNames = null;
-
if (item.VideoType == VideoType.Dvd)
{
- streamFileNames = FetchFromDvdLib(item);
+ // Get list of playable .vob files
+ var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null);
- if (streamFileNames.Length == 0)
+ // Return if no playable .vob files are found
+ if (vobs.Count == 0)
{
- _logger.LogError("No playable vobs found in dvd structure, skipping ffprobe.");
+ _logger.LogError("No playable .vob files found in DVD structure, skipping FFprobe.");
return ItemUpdateType.MetadataImport;
}
+
+ // Fetch metadata of first .vob file
+ mediaInfoResult = await GetMediaInfo(
+ new Video
+ {
+ Path = vobs[0]
+ },
+ cancellationToken).ConfigureAwait(false);
+
+ // Sum up the runtime of all .vob files skipping the first .vob
+ for (var i = 1; i < vobs.Count; i++)
+ {
+ var tmpMediaInfo = await GetMediaInfo(
+ new Video
+ {
+ Path = vobs[i]
+ },
+ cancellationToken).ConfigureAwait(false);
+
+ mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks;
+ }
}
else if (item.VideoType == VideoType.BluRay)
{
- var inputPath = item.Path;
-
- blurayDiscInfo = GetBDInfo(inputPath);
+ // Get BD disc information
+ blurayDiscInfo = GetBDInfo(item.Path);
- streamFileNames = blurayDiscInfo.Files;
+ // Get playable .m2ts files
+ var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path);
- if (streamFileNames.Length == 0)
+ // Return if no playable .m2ts files are found
+ if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0)
{
- _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe.");
+ _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe.");
return ItemUpdateType.MetadataImport;
}
- }
-
- streamFileNames ??= Array.Empty<string>();
- mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
+ // Fetch metadata of first .m2ts file
+ mediaInfoResult = await GetMediaInfo(
+ new Video
+ {
+ Path = m2ts[0]
+ },
+ cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
+ }
cancellationToken.ThrowIfCancellationRequested();
}
@@ -189,19 +218,8 @@ namespace MediaBrowser.Providers.MediaInfo
}
mediaAttachments = mediaInfo.MediaAttachments;
-
video.TotalBitrate = mediaInfo.Bitrate;
- // video.FormatName = (mediaInfo.Container ?? string.Empty)
- // .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
-
- // For DVDs this may not always be accurate, so don't set the runtime if the item already has one
- var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks is null || video.RunTimeTicks.Value == 0;
-
- if (needToSetRuntime)
- {
- video.RunTimeTicks = mediaInfo.RunTimeTicks;
- }
-
+ video.RunTimeTicks = mediaInfo.RunTimeTicks;
video.Size = mediaInfo.Size;
if (video.VideoType == VideoType.VideoFile)
@@ -280,7 +298,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
options.MetadataRefreshMode == MetadataRefreshMode.Default)
{
- if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
+ if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
{
chapters = CreateDummyChapters(video);
}
@@ -317,60 +335,58 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- private void FetchBdInfo(BaseItem item, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
+ private void FetchBdInfo(Video video, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
{
- var video = (Video)item;
-
- // video.PlayableStreamFileNames = blurayInfo.Files.ToList();
+ if (blurayInfo.Files.Length <= 1)
+ {
+ return;
+ }
// Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output
- if (blurayInfo.Files.Length > 1)
- {
- int? currentHeight = null;
- int? currentWidth = null;
- int? currentBitRate = null;
+ int? currentHeight = null;
+ int? currentWidth = null;
+ int? currentBitRate = null;
- var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+ var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
- // Grab the values that ffprobe recorded
- if (videoStream is not null)
- {
- currentBitRate = videoStream.BitRate;
- currentWidth = videoStream.Width;
- currentHeight = videoStream.Height;
- }
+ // Grab the values that ffprobe recorded
+ if (videoStream is not null)
+ {
+ currentBitRate = videoStream.BitRate;
+ currentWidth = videoStream.Width;
+ currentHeight = videoStream.Height;
+ }
- // Fill video properties from the BDInfo result
- mediaStreams.Clear();
- mediaStreams.AddRange(blurayInfo.MediaStreams);
+ // Fill video properties from the BDInfo result
+ mediaStreams.Clear();
+ mediaStreams.AddRange(blurayInfo.MediaStreams);
- if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0)
- {
- video.RunTimeTicks = blurayInfo.RunTimeTicks;
- }
+ if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0)
+ {
+ video.RunTimeTicks = blurayInfo.RunTimeTicks;
+ }
- if (blurayInfo.Chapters is not null)
+ if (blurayInfo.Chapters is not null)
+ {
+ double[] brChapter = blurayInfo.Chapters;
+ chapters = new ChapterInfo[brChapter.Length];
+ for (int i = 0; i < brChapter.Length; i++)
{
- double[] brChapter = blurayInfo.Chapters;
- chapters = new ChapterInfo[brChapter.Length];
- for (int i = 0; i < brChapter.Length; i++)
+ chapters[i] = new ChapterInfo
{
- chapters[i] = new ChapterInfo
- {
- StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks
- };
- }
+ StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks
+ };
}
+ }
- videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+ videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
- // Use the ffprobe values if these are empty
- if (videoStream is not null)
- {
- videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
- videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
- videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
- }
+ // Use the ffprobe values if these are empty
+ if (videoStream is not null)
+ {
+ videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
+ videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
+ videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
}
}
@@ -386,10 +402,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <returns>VideoStream.</returns>
private BlurayDiscInfo GetBDInfo(string path)
{
- if (string.IsNullOrWhiteSpace(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
+ ArgumentException.ThrowIfNullOrEmpty(path);
try
{
@@ -649,67 +662,39 @@ namespace MediaBrowser.Providers.MediaInfo
private ChapterInfo[] CreateDummyChapters(Video video)
{
var runtime = video.RunTimeTicks ?? 0;
- long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
- if (runtime < 0)
+ // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
+ if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
- "{0} has invalid runtime of {1}",
+ "{0} has an invalid runtime of {1} minutes",
video.Name,
- runtime));
+ TimeSpan.FromTicks(runtime).Minutes));
}
- if (runtime < dummyChapterDuration)
+ long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
+ if (runtime > dummyChapterDuration)
{
- return Array.Empty<ChapterInfo>();
- }
-
- // Limit the chapters just in case there's some incorrect metadata here
- int chapterCount = (int)Math.Min(runtime / dummyChapterDuration, _config.Configuration.DummyChapterCount);
- var chapters = new ChapterInfo[chapterCount];
+ int chapterCount = (int)(runtime / dummyChapterDuration);
+ var chapters = new ChapterInfo[chapterCount];
- long currentChapterTicks = 0;
- for (int i = 0; i < chapterCount; i++)
- {
- chapters[i] = new ChapterInfo
+ long currentChapterTicks = 0;
+ for (int i = 0; i < chapterCount; i++)
{
- StartPositionTicks = currentChapterTicks
- };
-
- currentChapterTicks += dummyChapterDuration;
- }
-
- return chapters;
- }
-
- private string[] FetchFromDvdLib(Video item)
- {
- var path = item.Path;
- var dvd = new Dvd(path);
-
- var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault();
+ chapters[i] = new ChapterInfo
+ {
+ StartPositionTicks = currentChapterTicks
+ };
- byte? titleNumber = null;
+ currentChapterTicks += dummyChapterDuration;
+ }
- if (primaryTitle is not null)
- {
- titleNumber = primaryTitle.VideoTitleSetNumber;
- item.RunTimeTicks = GetRuntime(primaryTitle);
+ return chapters;
}
- return _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, titleNumber)
- .Select(Path.GetFileName)
- .ToArray();
- }
-
- private long GetRuntime(Title title)
- {
- return title.ProgramChains
- .Select(i => (TimeSpan)i.PlaybackTime)
- .Select(i => i.Ticks)
- .Sum();
+ return Array.Empty<ChapterInfo>();
}
}
}
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 58cd23aa3..3476e7000 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -54,6 +54,12 @@ namespace MediaBrowser.Providers.Music
{
var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
+ // don't update user-changeable metadata for locked items
+ if (item.IsLocked)
+ {
+ return updateType;
+ }
+
if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
{
if (!item.LockedFields.Contains(MetadataField.Name))
diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
index db4c5f436..9bd36f25c 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
@@ -87,7 +87,7 @@ namespace MediaBrowser.Providers.Playlists
return GetPlsItems(stream);
}
- return new List<LinkedChild>();
+ return Enumerable.Empty<LinkedChild>();
}
private IEnumerable<LinkedChild> GetPlsItems(Stream stream)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
index b1a285a96..2232dfa0d 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
@@ -42,7 +43,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
+ return new ImageType[]
{
ImageType.Primary,
ImageType.Logo,
@@ -74,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
}
}
- return new List<RemoteImageInfo>();
+ return Enumerable.Empty<RemoteImageInfo>();
}
private IEnumerable<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
index eab252005..2093effca 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
@@ -1,12 +1,13 @@
<!DOCTYPE html>
<html>
<head>
- <title>AudioDB</title>
+ <title>TheAudioDB</title>
</head>
<body>
- <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div id="configPage" data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content">
<div class="content-primary">
+ <h1>TheAudioDB</h1>
<form class="configForm">
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="replaceAlbumName" />
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
index 22229e377..21a15c58c 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
@@ -1,5 +1,4 @@
using MediaBrowser.Model.Plugins;
-using MetaBrainz.MusicBrainz;
namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
@@ -8,16 +7,22 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
- private const string DefaultServer = "musicbrainz.org";
+ /// <summary>
+ /// The default server URL.
+ /// </summary>
+ public const string DefaultServer = "https://musicbrainz.org";
- private const double DefaultRateLimit = 1.0;
+ /// <summary>
+ /// The default rate limit.
+ /// </summary>
+ public const double DefaultRateLimit = 1.0;
private string _server = DefaultServer;
private double _rateLimit = DefaultRateLimit;
/// <summary>
- /// Gets or sets the server url.
+ /// Gets or sets the server URL.
/// </summary>
public string Server
{
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
index 6f1296bb7..24f2ac0ca 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
@@ -4,17 +4,18 @@
<title>MusicBrainz</title>
</head>
<body>
- <div data-role="page" class="page type-interior pluginConfigurationPage musicBrainzConfigPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div id="configPage" data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content">
<div class="content-primary">
- <form class="musicBrainzConfigForm">
+ <h1>MusicBrainz</h1>
+ <form class="configForm">
<div class="inputContainer">
<input is="emby-input" type="text" id="server" required label="Server" />
<div class="fieldDescription">This can be a mirror of the official server or even a custom server.</div>
</div>
<div class="inputContainer">
- <input is="emby-input" type="number" id="rateLimit" pattern="[0-9]*" required min="0" max="10000" label="Rate Limit" />
- <div class="fieldDescription">Span of time between requests in milliseconds. The official server is limited to one request every two seconds.</div>
+ <input is="emby-input" type="number" id="rateLimit" required pattern="[0-9]*" min="0" max="10" step=".01" label="Rate Limit" />
+ <div class="fieldDescription">Span of time between requests in seconds. The official server is limited to one request every seconds.</div>
</div>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="replaceArtistName" />
@@ -32,7 +33,7 @@
uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
};
- document.querySelector('.musicBrainzConfigPage')
+ document.querySelector('.configPage')
.addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
@@ -49,14 +50,14 @@
bubbles: true,
cancelable: false
}));
-
+
document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName;
Dashboard.hideLoadingMsg();
});
});
- document.querySelector('.musicBrainzConfigForm')
+ document.querySelector('.configForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 34f45f0d5..d0bd7d609 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -8,8 +8,10 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Music;
+using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
using MetaBrainz.MusicBrainz;
using MetaBrainz.MusicBrainz.Interfaces.Entities;
using MetaBrainz.MusicBrainz.Interfaces.Searches;
@@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
{
private readonly ILogger<MusicBrainzAlbumProvider> _logger;
- private readonly Query _musicBrainzQuery;
- private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org";
+ private Query _musicBrainzQuery;
/// <summary>
/// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
@@ -33,29 +34,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
public MusicBrainzAlbumProvider(ILogger<MusicBrainzAlbumProvider> logger)
{
_logger = logger;
-
- MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
- {
- if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server))
- {
- Query.DefaultServer = server.Host;
- Query.DefaultPort = server.Port;
- Query.DefaultUrlScheme = server.Scheme;
- }
- else
- {
- // Fallback to official server
- _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
- var defaultServer = new Uri(_musicBrainzDefaultUri);
- Query.DefaultServer = defaultServer.Host;
- Query.DefaultPort = defaultServer.Port;
- Query.DefaultUrlScheme = defaultServer.Scheme;
- }
-
- Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
- };
-
_musicBrainzQuery = new Query();
+ ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration);
+ MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig;
}
/// <inheritdoc />
@@ -64,6 +45,29 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
/// <inheritdoc />
public int Order => 0;
+ private void ReloadConfig(object? sender, BasePluginConfiguration e)
+ {
+ var configuration = (PluginConfiguration)e;
+ if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server))
+ {
+ Query.DefaultServer = server.DnsSafeHost;
+ Query.DefaultPort = server.Port;
+ Query.DefaultUrlScheme = server.Scheme;
+ }
+ else
+ {
+ // Fallback to official server
+ _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
+ var defaultServer = new Uri(PluginConfiguration.DefaultServer);
+ Query.DefaultServer = defaultServer.Host;
+ Query.DefaultPort = defaultServer.Port;
+ Query.DefaultUrlScheme = defaultServer.Scheme;
+ }
+
+ Query.DelayBetweenRequests = configuration.RateLimit;
+ _musicBrainzQuery = new Query();
+ }
+
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
{
@@ -72,13 +76,13 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
if (!string.IsNullOrEmpty(releaseId))
{
- var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
+ var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
}
if (!string.IsNullOrEmpty(releaseGroupId))
{
- var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false);
+ var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false);
return GetReleaseGroupResult(releaseGroupResult.Releases);
}
@@ -133,7 +137,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
foreach (var result in releaseSearchResults)
{
- yield return GetReleaseResult(result);
+ // Fetch full release info, otherwise artists are missing
+ var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups);
+ yield return GetReleaseResult(fullResult);
}
}
@@ -143,21 +149,33 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
{
Name = releaseSearchResult.Title,
ProductionYear = releaseSearchResult.Date?.Year,
- PremiereDate = releaseSearchResult.Date?.NearestDate
+ PremiereDate = releaseSearchResult.Date?.NearestDate,
+ SearchProviderName = Name
};
- if (releaseSearchResult.ArtistCredit?.Count > 0)
+ // Add artists and use first as album artist
+ var artists = releaseSearchResult.ArtistCredit;
+ if (artists is not null && artists.Count > 0)
{
- searchResult.AlbumArtist = new RemoteSearchResult
+ var artistResults = new RemoteSearchResult[artists.Count];
+ for (int i = 0; i < artists.Count; i++)
{
- SearchProviderName = Name,
- Name = releaseSearchResult.ArtistCredit[0].Name
- };
+ var artist = artists[i];
+ var artistResult = new RemoteSearchResult
+ {
+ Name = artist.Name
+ };
- if (releaseSearchResult.ArtistCredit[0].Artist?.Id is not null)
- {
- searchResult.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, releaseSearchResult.ArtistCredit[0].Artist!.Id.ToString());
+ if (artist.Artist?.Id is not null)
+ {
+ artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString());
+ }
+
+ artistResults[i] = artistResult;
}
+
+ searchResult.AlbumArtist = artistResults[0];
+ searchResult.Artists = artistResults;
}
searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString());
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
index 718b5a1c4..1323d2604 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
@@ -8,8 +8,10 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Music;
+using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
using MetaBrainz.MusicBrainz;
using MetaBrainz.MusicBrainz.Interfaces.Entities;
using MetaBrainz.MusicBrainz.Interfaces.Searches;
@@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable
{
private readonly ILogger<MusicBrainzArtistProvider> _logger;
- private readonly Query _musicBrainzQuery;
- private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org";
+ private Query _musicBrainzQuery;
/// <summary>
/// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class.
@@ -33,34 +34,37 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
public MusicBrainzArtistProvider(ILogger<MusicBrainzArtistProvider> logger)
{
_logger = logger;
-
- MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
- {
- if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server))
- {
- Query.DefaultServer = server.Host;
- Query.DefaultPort = server.Port;
- Query.DefaultUrlScheme = server.Scheme;
- }
- else
- {
- // Fallback to official server
- _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
- var defaultServer = new Uri(_musicBrainzDefaultUri);
- Query.DefaultServer = defaultServer.Host;
- Query.DefaultPort = defaultServer.Port;
- Query.DefaultUrlScheme = defaultServer.Scheme;
- }
-
- Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
- };
-
_musicBrainzQuery = new Query();
+ ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration);
+ MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig;
}
/// <inheritdoc />
public string Name => "MusicBrainz";
+ private void ReloadConfig(object? sender, BasePluginConfiguration e)
+ {
+ var configuration = (PluginConfiguration)e;
+ if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server))
+ {
+ Query.DefaultServer = server.DnsSafeHost;
+ Query.DefaultPort = server.Port;
+ Query.DefaultUrlScheme = server.Scheme;
+ }
+ else
+ {
+ // Fallback to official server
+ _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
+ var defaultServer = new Uri(PluginConfiguration.DefaultServer);
+ Query.DefaultServer = defaultServer.Host;
+ Query.DefaultPort = defaultServer.Port;
+ Query.DefaultUrlScheme = defaultServer.Scheme;
+ }
+
+ Query.DelayBetweenRequests = configuration.RateLimit;
+ _musicBrainzQuery = new Query();
+ }
+
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
{
@@ -112,7 +116,8 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
{
Name = artist.Name,
ProductionYear = artist.LifeSpan?.Begin?.Year,
- PremiereDate = artist.LifeSpan?.Begin?.NearestDate
+ PremiereDate = artist.LifeSpan?.Begin?.NearestDate,
+ SearchProviderName = Name,
};
searchResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Id.ToString());
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
index f4375b3cb..d00c1f9f8 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
@@ -4,9 +4,10 @@
<title>OMDb</title>
</head>
<body>
- <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div id="configPage" data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content">
<div class="content-primary">
+ <h1>OMDb</h1>
<form class="configForm">
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="castAndCrew" />
@@ -33,16 +34,16 @@
});
});
-
+
document.querySelector('.configForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
-
+
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
config.CastAndCrew = document.querySelector('#castAndCrew').checked;
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
});
-
+
e.preventDefault();
return false;
});
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
index 60b373483..140a64f52 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -38,10 +38,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
- {
- ImageType.Primary
- };
+ yield return ImageType.Primary;
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 497437bd8..dfaba6423 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -98,8 +98,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
// item.VoteCount = voteCount;
}
- if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
+ if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -209,8 +208,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
// item.VoteCount = voteCount;
}
- if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
+ if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -552,7 +550,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (rating?.Value is not null)
{
var value = rating.Value.TrimEnd('%');
- if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var score))
+ if (float.TryParse(value, CultureInfo.InvariantCulture, out var score))
{
return score;
}
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html
index 63750dbcd..85ebe443f 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html
@@ -4,9 +4,10 @@
<title>Studio Images</title>
</head>
<body>
- <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div id="configPage" data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content">
<div class="content-primary">
+ <h1>Studio Images</h1>
<form class="configForm">
<div class="inputContainer">
<input is="emby-input" type="text" id="repository" label="Repository" />
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index 0fb9d30a6..ae244da19 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
+ return new ImageType[]
{
ImageType.Thumb
};
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
index ac3df1d5d..450ee2a33 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
@@ -11,7 +11,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api
/// The TMDb API controller.
/// </summary>
[ApiController]
- [Authorize(Policy = "DefaultAuthorization")]
+ [Authorize]
[Route("[controller]")]
[Produces(MediaTypeNames.Application.Json)]
public class TmdbController : ControllerBase
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index eee3658de..a4c6cb47d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -50,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
+ return new ImageType[]
{
ImageType.Primary,
ImageType.Backdrop
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index 1cce7fc35..c2018d820 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -74,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
- var collections = new List<RemoteSearchResult>();
+ var collections = new RemoteSearchResult[collectionSearchResults.Count];
for (var i = 0; i < collectionSearchResults.Count; i++)
{
var collection = new RemoteSearchResult
@@ -84,7 +82,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
};
collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture));
- collections.Add(collection);
+ collections[i] = collection;
}
return collections;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
index 48ec0535c..cd21516f9 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -4,9 +4,10 @@
<title>TMDb</title>
</head>
<body>
- <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div id="configPage" data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content">
<div class="content-primary">
+ <h1>TMDb</h1>
<form class="configForm">
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="includeAdult" />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index 02601d3f5..bfec48e7c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -51,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
+ return new ImageType[]
{
ImageType.Primary,
ImageType.Backdrop,
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 9eced93fa..fc7202366 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -64,32 +62,35 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
cancellationToken)
.ConfigureAwait(false);
- var remoteResult = new RemoteSearchResult
+ if (movie is not null)
{
- Name = movie.Title ?? movie.OriginalTitle,
- SearchProviderName = Name,
- ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
- Overview = movie.Overview
- };
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = movie.Title ?? movie.OriginalTitle,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
+ Overview = movie.Overview
+ };
- if (movie.ReleaseDate is not null)
- {
- var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
- remoteResult.PremiereDate = releaseDate;
- remoteResult.ProductionYear = releaseDate.Year;
- }
+ if (movie.ReleaseDate is not null)
+ {
+ var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
+ remoteResult.PremiereDate = releaseDate;
+ remoteResult.ProductionYear = releaseDate.Year;
+ }
- remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
- if (!string.IsNullOrWhiteSpace(movie.ImdbId))
- {
- remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
- }
+ if (!string.IsNullOrWhiteSpace(movie.ImdbId))
+ {
+ remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
+ }
- return new[] { remoteResult };
+ return new[] { remoteResult };
+ }
}
- IReadOnlyList<SearchMovie> movieResults;
+ IReadOnlyList<SearchMovie>? movieResults = null;
if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out id))
{
var result = await _tmdbClientManager.FindByExternalIdAsync(
@@ -97,18 +98,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
FindExternalSource.Imdb,
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
cancellationToken).ConfigureAwait(false);
- movieResults = result.MovieResults;
+ movieResults = result?.MovieResults;
}
- else if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id))
+
+ if (movieResults is null && searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id))
{
var result = await _tmdbClientManager.FindByExternalIdAsync(
id,
FindExternalSource.TvDb,
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
cancellationToken).ConfigureAwait(false);
- movieResults = result.MovieResults;
+ movieResults = result?.MovieResults;
}
- else
+
+ if (movieResults is null)
{
movieResults = await _tmdbClientManager
.SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index bc959ee2b..9e5404b32 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -46,10 +46,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
- {
- ImageType.Primary
- };
+ yield return ImageType.Primary;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index b3709baf5..5c6e71fd8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -69,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
- var remoteSearchResults = new List<RemoteSearchResult>();
+ var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count];
for (var i = 0; i < personSearchResult.Count; i++)
{
var person = personSearchResult[i];
@@ -81,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
};
remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
- remoteSearchResults.Add(remoteSearchResult);
+ remoteSearchResults[i] = remoteSearchResult;
}
return remoteSearchResults;
@@ -107,6 +105,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId > 0)
{
var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ if (person is null)
+ {
+ return result;
+ }
result.HasMetadata = true;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index 5259faf76..d1fec7cb1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -49,10 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
- {
- ImageType.Primary
- };
+ yield return ImageType.Primary;
}
/// <inheritdoc />
@@ -63,7 +58,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (seriesTmdbId <= 0)
+ if (series is null || seriesTmdbId <= 0)
{
return Enumerable.Empty<RemoteImageInfo>();
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 35e304a2a..66decde84 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -87,7 +85,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return metadataResult;
}
- info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string tmdbId);
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? tmdbId);
var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture);
if (seriesTmdbId <= 0)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index b8d1460db..a743601ed 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -48,10 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
- {
- ImageType.Primary
- };
+ yield return ImageType.Primary;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 79cb6e86d..192fb052d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
+ return new ImageType[]
{
ImageType.Primary,
ImageType.Backdrop,
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 959088210..09d1a739d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -211,7 +209,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
- if (string.IsNullOrEmpty(tmdbId))
+ if (!int.TryParse(tmdbId, CultureInfo.InvariantCulture, out int tmdbIdInt))
{
return result;
}
@@ -219,9 +217,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
cancellationToken.ThrowIfCancellationRequested();
var tvShow = await _tmdbClientManager
- .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.ConfigureAwait(false);
+ if (tvShow is null)
+ {
+ return result;
+ }
+
result = new MetadataResult<Series>
{
Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index c7441bf35..500ebaf71 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -1,6 +1,4 @@
-#nullable disable
-
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
@@ -50,10 +48,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie or null if not found.</returns>
- public async Task<Movie> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ public async Task<Movie?> GetMovieAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
{
var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
- if (_memoryCache.TryGetValue(key, out Movie movie))
+ if (_memoryCache.TryGetValue(key, out Movie? movie))
{
return movie;
}
@@ -89,10 +87,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb collection or null if not found.</returns>
- public async Task<Collection> GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ public async Task<Collection?> GetCollectionAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
{
var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
- if (_memoryCache.TryGetValue(key, out Collection collection))
+ if (_memoryCache.TryGetValue(key, out Collection? collection))
{
return collection;
}
@@ -122,10 +120,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information or null if not found.</returns>
- public async Task<TvShow> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ public async Task<TvShow?> GetSeriesAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
{
var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
- if (_memoryCache.TryGetValue(key, out TvShow series))
+ if (_memoryCache.TryGetValue(key, out TvShow? series))
{
return series;
}
@@ -162,7 +160,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show episode group information or null if not found.</returns>
- private async Task<TvGroupCollection> GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken)
+ private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken)
{
TvGroupType? groupType =
string.Equals(displayOrder, "originalAirDate", StringComparison.Ordinal) ? TvGroupType.OriginalAirDate :
@@ -180,7 +178,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
- if (_memoryCache.TryGetValue(key, out TvGroupCollection group))
+ if (_memoryCache.TryGetValue(key, out TvGroupCollection? group))
{
return group;
}
@@ -217,10 +215,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv season information or null if not found.</returns>
- public async Task<TvSeason> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken)
+ public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string? language, string? imageLanguages, CancellationToken cancellationToken)
{
var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
- if (_memoryCache.TryGetValue(key, out TvSeason season))
+ if (_memoryCache.TryGetValue(key, out TvSeason? season))
{
return season;
}
@@ -254,10 +252,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv episode information or null if not found.</returns>
- public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken)
+ public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken)
{
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
- if (_memoryCache.TryGetValue(key, out TvEpisode episode))
+ if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
{
return episode;
}
@@ -301,10 +299,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="language">The episode's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb person information or null if not found.</returns>
- public async Task<Person> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken)
+ public async Task<Person?> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken)
{
var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
- if (_memoryCache.TryGetValue(key, out Person person))
+ if (_memoryCache.TryGetValue(key, out Person? person))
{
return person;
}
@@ -333,14 +331,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="language">The item's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb item or null if not found.</returns>
- public async Task<FindContainer> FindByExternalIdAsync(
+ public async Task<FindContainer?> FindByExternalIdAsync(
string externalId,
FindExternalSource source,
string language,
CancellationToken cancellationToken)
{
var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
- if (_memoryCache.TryGetValue(key, out FindContainer result))
+ if (_memoryCache.TryGetValue(key, out FindContainer? result))
{
return result;
}
@@ -372,7 +370,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default)
{
var key = $"searchseries-{name}-{language}";
- if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null)
{
return series.Results;
}
@@ -400,7 +398,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
{
var key = $"searchperson-{name}";
- if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person))
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null)
{
return person.Results;
}
@@ -442,7 +440,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
{
var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
- if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies))
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null)
{
return movies.Results;
}
@@ -471,7 +469,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
{
var key = $"collectionsearch-{name}-{language}";
- if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections))
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null)
{
return collections.Results;
}
@@ -496,7 +494,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="size">The image size to fetch.</param>
/// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns>
- private string GetUrl(string size, string path)
+ private string? GetUrl(string? size, string path)
{
if (string.IsNullOrEmpty(path))
{
@@ -511,7 +509,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns>
- public string GetPosterUrl(string posterPath)
+ public string? GetPosterUrl(string posterPath)
{
return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
}
@@ -521,7 +519,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="actorProfilePath">The relative URL of the profile image.</param>
/// <returns>The absolute URL.</returns>
- public string GetProfileUrl(string actorProfilePath)
+ public string? GetProfileUrl(string actorProfilePath)
{
return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
}
@@ -579,7 +577,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="type">The type of the image.</param>
/// <param name="requestLanguage">The requested language.</param>
/// <returns>The remote images.</returns>
- private IEnumerable<RemoteImageInfo> ConvertToRemoteImageInfo(IReadOnlyList<ImageData> images, string size, ImageType type, string requestLanguage)
+ private IEnumerable<RemoteImageInfo> ConvertToRemoteImageInfo(IReadOnlyList<ImageData> images, string? size, ImageType type, string requestLanguage)
{
// sizes provided are for original resolution, don't store them when downloading scaled images
var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index 44c2c81f4..b326d22c8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using MediaBrowser.Model.Entities;
using TMDbLib.Objects.General;
@@ -128,7 +129,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="language">The language code.</param>
/// <returns>The normalized language code.</returns>
- public static string NormalizeLanguage(string language)
+ [return: NotNullIfNotNull(nameof(language))]
+ public static string? NormalizeLanguage(string? language)
{
if (string.IsNullOrEmpty(language))
{
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index b1a26cfba..0c01c5031 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -35,34 +33,30 @@ namespace MediaBrowser.Providers.Subtitles
private readonly IMediaSourceManager _mediaSourceManager;
private readonly ILocalizationManager _localization;
- private ISubtitleProvider[] _subtitleProviders;
+ private readonly ISubtitleProvider[] _subtitleProviders;
public SubtitleManager(
ILogger<SubtitleManager> logger,
IFileSystem fileSystem,
ILibraryMonitor monitor,
IMediaSourceManager mediaSourceManager,
- ILocalizationManager localizationManager)
+ ILocalizationManager localizationManager,
+ IEnumerable<ISubtitleProvider> subtitleProviders)
{
_logger = logger;
_fileSystem = fileSystem;
_monitor = monitor;
_mediaSourceManager = mediaSourceManager;
_localization = localizationManager;
- }
-
- /// <inheritdoc />
- public event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure;
-
- /// <inheritdoc />
- public void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders)
- {
_subtitleProviders = subtitleProviders
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
.ToArray();
}
/// <inheritdoc />
+ public event EventHandler<SubtitleDownloadFailureEventArgs>? SubtitleDownloadFailure;
+
+ /// <inheritdoc />
public async Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken)
{
if (request.Language is not null)
@@ -197,49 +191,49 @@ namespace MediaBrowser.Providers.Subtitles
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
}
- }
- var savePaths = new List<string>();
- var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
+ var savePaths = new List<string>();
+ var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
- if (response.IsForced)
- {
- saveFileName += ".forced";
- }
+ if (response.IsForced)
+ {
+ saveFileName += ".forced";
+ }
- saveFileName += "." + response.Format.ToLowerInvariant();
+ saveFileName += "." + response.Format.ToLowerInvariant();
- if (saveInMediaFolder)
- {
- var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
- // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path.");
- if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal))
+ if (saveInMediaFolder)
{
- savePaths.Add(mediaFolderPath);
+ var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
+ // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path.");
+ if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal))
+ {
+ savePaths.Add(mediaFolderPath);
+ }
}
- }
- var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
+ var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
- // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path.");
- if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
- {
- savePaths.Add(internalPath);
- }
+ // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path.");
+ if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
+ {
+ savePaths.Add(internalPath);
+ }
- if (savePaths.Count > 0)
- {
- await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
- }
- else
- {
- _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
+ if (savePaths.Count > 0)
+ {
+ await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
+ }
+ else
+ {
+ _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
+ }
}
}
private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
{
- List<Exception> exs = null;
+ List<Exception>? exs = null;
foreach (var savePath in savePaths)
{
@@ -249,7 +243,7 @@ namespace MediaBrowser.Providers.Subtitles
try
{
- Directory.CreateDirectory(Path.GetDirectoryName(savePath));
+ Directory.CreateDirectory(Path.GetDirectoryName(savePath) ?? throw new InvalidOperationException("Path can't be a root directory."));
var fileOptions = AsyncFile.WriteOptions;
fileOptions.Mode = FileMode.CreateNew;
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index a261d7cdb..97f938397 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 6e82d96d1..807234915 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -22,13 +22,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 96c512a46..8bd30447a 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -266,11 +266,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var nfoConfiguration = _config.GetNfoConfiguration();
UserItemData? userData = null;
- if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
- {
- var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
- userData = _userDataManager.GetUserData(user, item);
- }
switch (reader.Name)
{
@@ -279,16 +274,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
- {
- item.DateCreated = added;
- }
- else
- {
- Logger.LogWarning("Invalid Added value found: {Value}", val);
- }
+ item.DateCreated = added;
+ }
+ else
+ {
+ Logger.LogWarning("Invalid Added value found: {Value}", val);
}
break;
@@ -320,12 +312,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var text = reader.ReadElementContentAsString();
- if (!string.IsNullOrEmpty(text))
+ if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
{
- if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
- {
- item.CriticRating = value;
- }
+ item.CriticRating = value;
}
break;
@@ -370,9 +359,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsBoolean();
- if (userData is not null)
+ if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
{
+ var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
+ userData = _userDataManager.GetUserData(user, item);
userData.Played = val;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
break;
@@ -381,12 +373,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "playcount":
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val) && userData is not null)
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)
+ && Guid.TryParse(nfoConfiguration.UserId, out var guid))
{
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count))
- {
- userData.PlayCount = count;
- }
+ var user = _userManager.GetUserById(guid);
+ userData = _userDataManager.GetUserData(user, item);
+ userData.PlayCount = count;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
break;
@@ -395,12 +388,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "lastplayed":
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val) && userData is not null)
+ if (Guid.TryParse(nfoConfiguration.UserId, out var guid))
{
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
+ var user = _userManager.GetUserById(guid);
+ userData = _userDataManager.GetUserData(user, item);
userData.LastPlayedDate = added;
- }
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
+ }
else
{
Logger.LogWarning("Invalid lastplayed value found: {Value}", val);
@@ -486,12 +482,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var text = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(text))
+ if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
- if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
- {
- item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
- }
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
break;
@@ -629,13 +622,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- var hasDisplayOrder = item as IHasDisplayOrder;
- if (hasDisplayOrder is not null)
+ if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val))
{
- if (!string.IsNullOrWhiteSpace(val))
- {
- hasDisplayOrder.DisplayOrder = val;
- }
+ hasDisplayOrder.DisplayOrder = val;
}
break;
@@ -645,12 +634,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (int.TryParse(val, out var productionYear) && productionYear > 1850)
{
- if (int.TryParse(val, out var productionYear) && productionYear > 1850)
- {
- item.ProductionYear = productionYear;
- }
+ item.ProductionYear = productionYear;
}
break;
@@ -660,13 +646,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var rating = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(rating))
+ // All external meta is saving this as '.' for decimal I believe...but just to be sure
+ if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
{
- // All external meta is saving this as '.' for decimal I believe...but just to be sure
- if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
- {
- item.CommunityRating = val;
- }
+ item.CommunityRating = val;
}
break;
@@ -696,13 +679,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.PremiereDate = date;
- item.ProductionYear = date.Year;
- }
+ item.PremiereDate = date;
+ item.ProductionYear = date.Year;
}
break;
@@ -714,12 +694,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.EndDate = date;
- }
+ item.EndDate = date;
}
break;
@@ -1190,21 +1167,21 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "value":
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
{
- if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
+ // if ratingName contains tomato --> assume critic rating
+ if (ratingName is not null
+ && ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase)
+ && !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
{
- // if ratingName contains tomato --> assume critic rating
- if (ratingName is not null &&
- ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) &&
- !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
+ if (!ratingName.Contains("avg", StringComparison.OrdinalIgnoreCase))
{
item.CriticRating = ratingValue;
}
- else
- {
- item.CommunityRating = ratingValue;
- }
+ }
+ else
+ {
+ item.CommunityRating = ratingValue;
}
}
@@ -1288,12 +1265,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
- {
- sortOrder = intVal;
- }
+ sortOrder = intVal;
}
break;
diff --git a/debian/bin/restart.sh b/debian/bin/restart.sh
deleted file mode 100755
index 4847b918b..000000000
--- a/debian/bin/restart.sh
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/bin/bash
-
-# restart.sh - Jellyfin server restart script
-# Part of the Jellyfin project (https://github.com/jellyfin)
-#
-# This script restarts the Jellyfin daemon on Linux when using
-# the Restart button on the admin dashboard. It supports the
-# systemctl, service, and traditional /etc/init.d (sysv) restart
-# methods, chosen automatically by which one is found first (in
-# that order).
-#
-# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
-
-# This is the Right Way(tm) to check if we are booted with
-# systemd, according to sd_booted(3)
-if [ -d /run/systemd/system ]; then
- cmd=systemctl
-else
- # Everything else is really hard to figure out, so we just use
- # service(8) if it's available - that works with most init
- # systems/distributions I know of, including FreeBSD
- if type service >/dev/null 2>&1; then
- cmd=service
- else
- # If even service(8) isn't available, we just try /etc/init.d
- # and hope for the best
- if [ -d /etc/init.d ]; then
- cmd=sysv
- else
- echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
- echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
- exit 1
- fi
- fi
-fi
-
-if type sudo >/dev/null 2>&1; then
- sudo_command=sudo
-else
- sudo_command=
-fi
-
-echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
-case $cmd in
- 'systemctl')
- # Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
- $sudo_command systemd-run systemctl restart jellyfin
- ;;
- 'service')
- echo "sleep 0.5; $sudo_command service jellyfin start" | at now
- ;;
- 'sysv')
- echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
- ;;
-esac
-exit 0
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index 2f0630a9c..912996755 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -21,9 +21,6 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
# web client path, installed by the jellyfin-web package
JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
-# Restart script for in-app server control
-JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
-
# ffmpeg binary paths, overriding the system values
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
@@ -50,4 +47,4 @@ JELLYFIN_ADDITIONAL_OPTS=""
# Application username
JELLYFIN_USER="jellyfin"
# Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS"
diff --git a/debian/conf/jellyfin-sudoers b/debian/conf/jellyfin-sudoers
deleted file mode 100644
index 795fd17e8..000000000
--- a/debian/conf/jellyfin-sudoers
+++ /dev/null
@@ -1,33 +0,0 @@
-#Allow jellyfin group to start, stop and restart itself
-Cmnd_Alias RESTARTSERVER_SYSV = /sbin/service jellyfin restart, /usr/sbin/service jellyfin restart
-Cmnd_Alias STARTSERVER_SYSV = /sbin/service jellyfin start, /usr/sbin/service jellyfin start
-Cmnd_Alias STOPSERVER_SYSV = /sbin/service jellyfin stop, /usr/sbin/service jellyfin stop
-Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl restart jellyfin
-Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl start jellyfin
-Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemd-run systemctl stop jellyfin
-Cmnd_Alias RESTARTSERVER_INITD = /etc/init.d/jellyfin restart
-Cmnd_Alias STARTSERVER_INITD = /etc/init.d/jellyfin start
-Cmnd_Alias STOPSERVER_INITD = /etc/init.d/jellyfin stop
-
-
-jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSV
-jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSV
-jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSV
-jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
-jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
-jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSTEMD
-jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_INITD
-jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_INITD
-jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_INITD
-
-Defaults!RESTARTSERVER_SYSV !requiretty
-Defaults!STARTSERVER_SYSV !requiretty
-Defaults!STOPSERVER_SYSV !requiretty
-Defaults!RESTARTSERVER_SYSTEMD !requiretty
-Defaults!STARTSERVER_SYSTEMD !requiretty
-Defaults!STOPSERVER_SYSTEMD !requiretty
-Defaults!RESTARTSERVER_INITD !requiretty
-Defaults!STARTSERVER_INITD !requiretty
-Defaults!STOPSERVER_INITD !requiretty
-
-Defaults:jellyfin !requiretty
diff --git a/debian/control b/debian/control
index 08c0dcda6..0b9dd570e 100644
--- a/debian/control
+++ b/debian/control
@@ -18,11 +18,10 @@ Package: jellyfin-server
Replaces: jellyfin (<<10.6.0)
Breaks: jellyfin (<<10.6.0)
Architecture: any
-Depends: at,
- libsqlite3-0,
+Depends: libsqlite3-0,
libfontconfig1,
libfreetype6,
libssl1.1 | libssl3
-Recommends: jellyfin-web, sudo
+Recommends: jellyfin-web
Description: Jellyfin is the Free Software Media System.
This package provides the Jellyfin server backend and API.
diff --git a/debian/install b/debian/install
index 994322d14..0b48dd7a2 100644
--- a/debian/install
+++ b/debian/install
@@ -2,5 +2,3 @@ usr/lib/jellyfin usr/lib/
debian/conf/jellyfin etc/default/
debian/conf/logging.json etc/jellyfin/
debian/conf/jellyfin.service.conf etc/systemd/system/jellyfin.service.d/
-debian/conf/jellyfin-sudoers etc/sudoers.d/
-debian/bin/restart.sh usr/lib/jellyfin/
diff --git a/debian/jellyfin.service b/debian/jellyfin.service
index 1150924a0..2cc49f7c4 100644
--- a/debian/jellyfin.service
+++ b/debian/jellyfin.service
@@ -8,7 +8,7 @@ EnvironmentFile = /etc/default/jellyfin
User = jellyfin
Group = jellyfin
WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
+ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
Restart = on-failure
TimeoutSec = 15
SuccessExitStatus=0 143
diff --git a/debian/postinst b/debian/postinst
index a15442c76..947959aa7 100644
--- a/debian/postinst
+++ b/debian/postinst
@@ -59,8 +59,6 @@ case "$1" in
chgrp adm $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
chmod 0750 $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
- chmod +x /usr/lib/jellyfin/restart.sh > /dev/null 2>&1 || true
-
# Install jellyfin symlink into /usr/bin
ln -sf /usr/lib/jellyfin/bin/jellyfin /usr/bin/jellyfin
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index e02087a52..36435194f 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 6962b6bc1..c8943a399 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 96e3ca403..7b9a3de4e 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index f1c536399..32695e3f1 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index eaea305d1..8ffbeafad 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fedora/jellyfin.env b/fedora/jellyfin.env
index 1ccd8196f..1f79fac4f 100644
--- a/fedora/jellyfin.env
+++ b/fedora/jellyfin.env
@@ -23,9 +23,6 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
# web client path, installed by the jellyfin-web package
# JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
-# In-App service control
-JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
-
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service
index 2fb9f30e0..1b3f8032c 100644
--- a/fedora/jellyfin.service
+++ b/fedora/jellyfin.service
@@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin
User = jellyfin
Group = jellyfin
WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
+ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
Restart = on-failure
TimeoutSec = 15
SuccessExitStatus=0 143
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 08de71537..a759b29b1 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -16,11 +16,9 @@ URL: https://jellyfin.org
Source0: jellyfin-server-%{version}.tar.gz
Source11: jellyfin.service
Source12: jellyfin.env
-Source13: jellyfin.sudoers
-Source14: restart.sh
-Source15: jellyfin.override.conf
-Source16: jellyfin-firewalld.xml
-Source17: jellyfin-server-lowports.conf
+Source13: jellyfin.override.conf
+Source14: jellyfin-firewalld.xml
+Source15: jellyfin-server-lowports.conf
%{?systemd_requires}
BuildRequires: systemd
@@ -44,7 +42,7 @@ Jellyfin is a free software media system that puts you in control of managing an
Summary: The Free Software Media System Server backend
Requires(pre): shadow-utils
Requires: ffmpeg
-Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu, at, sudo
+Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu
%description server
The Jellyfin media server backend.
@@ -76,16 +74,14 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
-%{__install} -D %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
# Jellyfin config
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
%{__install} -D %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin
# system config
-%{__install} -D %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
-%{__install} -D %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/jellyfin-sudoers
-%{__install} -D %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
+%{__install} -D %{SOURCE14} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
+%{__install} -D %{SOURCE13} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
%{__install} -D %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service
# empty directories
@@ -95,7 +91,7 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
%{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
# jellyfin-server-lowports subpackage
-%{__install} -D -m 0644 %{SOURCE17} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
+%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
%files
@@ -110,7 +106,6 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
%attr(755,root,root) %{_libdir}/jellyfin/createdump
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
%{_libdir}/jellyfin/*
-%attr(755,root,root) %{_libexecdir}/jellyfin/restart.sh
# Jellyfin config
%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
@@ -119,7 +114,6 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
# system config
%{_prefix}/lib/firewalld/services/jellyfin.xml
%{_unitdir}/jellyfin.service
-%config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/jellyfin-sudoers
%config(noreplace) %{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
# empty directories
diff --git a/fedora/jellyfin.sudoers b/fedora/jellyfin.sudoers
deleted file mode 100644
index 01c7f4e11..000000000
--- a/fedora/jellyfin.sudoers
+++ /dev/null
@@ -1,14 +0,0 @@
-# Allow jellyfin group to start, stop and restart itself
-Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl restart jellyfin
-Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl start jellyfin
-Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemd-run systemctl stop jellyfin
-
-jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
-jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
-jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSTEMD
-
-Defaults!RESTARTSERVER_SYSTEMD !requiretty
-Defaults!STARTSERVER_SYSTEMD !requiretty
-Defaults!STOPSERVER_SYSTEMD !requiretty
-
-Defaults:jellyfin !requiretty
diff --git a/fedora/restart.sh b/fedora/restart.sh
deleted file mode 100755
index 4847b918b..000000000
--- a/fedora/restart.sh
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/bin/bash
-
-# restart.sh - Jellyfin server restart script
-# Part of the Jellyfin project (https://github.com/jellyfin)
-#
-# This script restarts the Jellyfin daemon on Linux when using
-# the Restart button on the admin dashboard. It supports the
-# systemctl, service, and traditional /etc/init.d (sysv) restart
-# methods, chosen automatically by which one is found first (in
-# that order).
-#
-# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
-
-# This is the Right Way(tm) to check if we are booted with
-# systemd, according to sd_booted(3)
-if [ -d /run/systemd/system ]; then
- cmd=systemctl
-else
- # Everything else is really hard to figure out, so we just use
- # service(8) if it's available - that works with most init
- # systems/distributions I know of, including FreeBSD
- if type service >/dev/null 2>&1; then
- cmd=service
- else
- # If even service(8) isn't available, we just try /etc/init.d
- # and hope for the best
- if [ -d /etc/init.d ]; then
- cmd=sysv
- else
- echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
- echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
- exit 1
- fi
- fi
-fi
-
-if type sudo >/dev/null 2>&1; then
- sudo_command=sudo
-else
- sudo_command=
-fi
-
-echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
-case $cmd in
- 'systemctl')
- # Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
- $sudo_command systemd-run systemctl restart jellyfin
- ;;
- 'service')
- echo "sleep 0.5; $sudo_command service jellyfin start" | at now
- ;;
- 'sysv')
- echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
- ;;
-esac
-exit 0
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
index 9c2449da4..1e3f8a048 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -16,10 +16,10 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.17.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="SharpFuzz" Version="2.0.0" />
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="SharpFuzz" />
</ItemGroup>
</Project>
diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
index 5e7d14b11..20bc4c724 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
+++ b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="SharpFuzz" Version="2.0.0" />
+ <PackageReference Include="SharpFuzz" />
</ItemGroup>
</Project>
diff --git a/nuget.config b/nuget.config
deleted file mode 100644
index 326331f32..000000000
--- a/nuget.config
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<configuration>
- <packageSources>
- <add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
- </packageSources>
-</configuration>
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index c686b229a..3b0333299 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -16,11 +16,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BlurHashSharp" Version="1.2.0" />
- <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
- <PackageReference Include="SkiaSharp" Version="2.88.3" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
- <PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
+ <PackageReference Include="BlurHashSharp" />
+ <PackageReference Include="BlurHashSharp.SkiaSharp" />
+ <PackageReference Include="SkiaSharp" />
+ <PackageReference Include="SkiaSharp.NativeAssets.Linux" />
+ <PackageReference Include="SkiaSharp.Svg" />
</ItemGroup>
<ItemGroup>
@@ -31,13 +31,13 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
deleted file mode 100644
index 5bb42fb99..000000000
--- a/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using MediaBrowser.Model.Drawing;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia;
-
-/// <summary>
-/// Static helper class for drawing 'played' indicators.
-/// </summary>
-public static class PlayedIndicatorDrawer
-{
- private const int OffsetFromTopRightCorner = 38;
-
- /// <summary>
- /// Draw a 'played' indicator in the top right corner of a canvas.
- /// </summary>
- /// <param name="canvas">The canvas to draw the indicator on.</param>
- /// <param name="imageSize">
- /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
- /// indicator.
- /// </param>
- public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
- {
- var x = imageSize.Width - OffsetFromTopRightCorner;
-
- using var paint = new SKPaint
- {
- Color = SKColor.Parse("#CC00A4DC"),
- Style = SKPaintStyle.Fill
- };
-
- canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
-
- paint.Color = new SKColor(255, 255, 255, 255);
- paint.TextSize = 30;
- paint.IsAntialias = true;
-
- // or:
- // var emojiChar = 0x1F680;
- const string Text = "✔️";
- var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
-
- // ask the font manager for a font with that character
- paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
-
- canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint);
- }
-}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index ddb8a98d4..6da77ad95 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -399,7 +399,7 @@ public class SkiaEncoder : IImageEncoder
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
var blur = options.Blur ?? 0;
- var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
+ var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
if (bitmap is null)
@@ -522,11 +522,7 @@ public class SkiaEncoder : IImageEncoder
{
var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
- if (options.AddPlayedIndicator)
- {
- PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
- }
- else if (options.UnplayedCount.HasValue)
+ if (options.UnplayedCount.HasValue)
{
UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index b381c9ae7..533baba4f 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -5,10 +5,12 @@ using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.MediaEncoding;
@@ -36,7 +38,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder;
- private readonly IMediaEncoder _mediaEncoder;
+
+ private readonly SemaphoreSlim _parallelEncodingLimit;
private bool _disposed;
@@ -48,18 +51,27 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <param name="fileSystem">The filesystem.</param>
/// <param name="imageEncoder">The image encoder.</param>
/// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="config">The configuration.</param>
public ImageProcessor(
ILogger<ImageProcessor> logger,
IServerApplicationPaths appPaths,
IFileSystem fileSystem,
IImageEncoder imageEncoder,
- IMediaEncoder mediaEncoder)
+ IMediaEncoder mediaEncoder,
+ IServerConfigurationManager config)
{
_logger = logger;
_fileSystem = fileSystem;
_imageEncoder = imageEncoder;
- _mediaEncoder = mediaEncoder;
_appPaths = appPaths;
+
+ var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
+ if (semaphoreCount < 1)
+ {
+ semaphoreCount = 2 * Environment.ProcessorCount;
+ }
+
+ _parallelEncodingLimit = new(semaphoreCount, semaphoreCount);
}
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
@@ -188,7 +200,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
quality,
dateModified,
outputFormat,
- options.AddPlayedIndicator,
options.PercentPlayed,
options.UnplayedCount,
options.Blur,
@@ -199,7 +210,18 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
{
if (!File.Exists(cacheFilePath))
{
- string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
+ // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
+ await _parallelEncodingLimit.WaitAsync().ConfigureAwait(false);
+
+ string resultPath;
+ try
+ {
+ resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
+ }
+ finally
+ {
+ _parallelEncodingLimit.Release();
+ }
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
{
@@ -270,7 +292,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
int quality,
DateTime dateModified,
ImageFormat format,
- bool addPlayedIndicator,
double percentPlayed,
int? unwatchedCount,
int? blur,
@@ -325,11 +346,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
filename.Append(fillHeight.Value);
}
- if (addPlayedIndicator)
- {
- filename.Append(",pl=true");
- }
-
if (percentPlayed > 0)
{
filename.Append(",p=");
@@ -563,6 +579,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
disposable.Dispose();
}
+ _parallelEncodingLimit?.Dispose();
+
_disposed = true;
}
}
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index a5bc8eaa7..e0963ac34 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -23,13 +23,13 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/src/Jellyfin.Extensions/AlphanumericComparator.cs b/src/Jellyfin.Extensions/AlphanumericComparator.cs
index 1b19752bb..6e451d40e 100644
--- a/src/Jellyfin.Extensions/AlphanumericComparator.cs
+++ b/src/Jellyfin.Extensions/AlphanumericComparator.cs
@@ -86,47 +86,12 @@ namespace Jellyfin.Extensions
{
return 1;
}
- else if (span1Len >= 20) // Number is probably too big for a ulong
- {
- // Trim all the first digits that are the same
- int i = 0;
- while (i < span1Len && span1[i] == span2[i])
- {
- i++;
- }
-
- // If there are no more digits it's the same number
- if (i == span1Len)
- {
- continue;
- }
-
- // Only need to compare the most significant digit
- span1 = span1.Slice(i, 1);
- span2 = span2.Slice(i, 1);
- }
-
- if (!ulong.TryParse(span1, out var num1)
- || !ulong.TryParse(span2, out var num2))
- {
- return 0;
- }
- else if (num1 < num2)
- {
- return -1;
- }
- else if (num1 > num2)
- {
- return 1;
- }
}
- else
+
+ int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
+ if (result != 0)
{
- int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
- if (result != 0)
- {
- return result;
- }
+ return result;
}
} while (pos1 < len1 && pos2 < len2);
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 9fed8cbd9..4f80aa941 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -29,18 +29,18 @@
<ItemGroup>
- <PackageReference Include="Diacritics" Version="3.3.14" />
+ <PackageReference Include="Diacritics" />
</ItemGroup>
<!-- Code Analyzers-->
- <ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs
new file mode 100644
index 000000000..2936fe4d6
--- /dev/null
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Extensions.Json.Converters;
+
+/// <summary>
+/// Converts a string to a boolean.
+/// This is needed for FFprobe.
+/// </summary>
+public class JsonBoolStringConverter : JsonConverter<bool>
+{
+ /// <inheritdoc />
+ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ ReadOnlySpan<byte> utf8Span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+ if (Utf8Parser.TryParse(utf8Span, out bool val, out _, 'l'))
+ {
+ return val;
+ }
+ }
+
+ return reader.GetBoolean();
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
+ => writer.WriteBooleanValue(value);
+}
diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
index 97cbee971..4d56ca615 100644
--- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs
+++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
@@ -39,7 +39,6 @@ namespace Jellyfin.Extensions.Json
new JsonFlagEnumConverterFactory(),
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
- new JsonBoolNumberConverter(),
new JsonDateTimeConverter(),
new JsonStringConverter()
}
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index f30b63945..7c6124875 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.Extensions
{
// Matches non-conforming unicode chars
// https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
- private static readonly Regex _nonConformingUnicode = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(\ufffd)");
+ private static readonly Regex _nonConformingUnicode = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(\ufffd)", RegexOptions.Compiled);
/// <summary>
/// Removes the diacritics character from the strings.
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index 32f80812a..3f4f55ee4 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -6,14 +6,14 @@
</PropertyGroup>
<!-- Code Analyzers-->
- <ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
<ItemGroup>
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index b11bdc477..71572bcf6 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -6,22 +6,22 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="NEbml" Version="0.11.0" />
+ <PackageReference Include="NEbml" />
</ItemGroup>
<!-- Code Analyzers-->
- <ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
new file mode 100644
index 000000000..de8fc1bb8
--- /dev/null
+++ b/tests/Directory.Build.props
@@ -0,0 +1,23 @@
+<Project>
+ <!-- Sets defaults for all test projects -->
+
+ <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <!-- Code Analyzers -->
+ <ItemGroup>
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
index 7c85ddd62..ad8a051fd 100644
--- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -1,9 +1,13 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
using Jellyfin.Server.Implementations.Security;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
@@ -51,6 +55,32 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
Assert.True(context.HasSucceeded);
}
+ [Fact]
+ public async Task ShouldSucceedOnApiKey()
+ {
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+
+ _httpContextAccessor
+ .Setup(h => h.HttpContext!.Connection.RemoteIpAddress)
+ .Returns(new IPAddress(0));
+
+ _userManagerMock
+ .Setup(u => u.GetUserById(It.IsAny<Guid>()))
+ .Returns<User>(null);
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.IsApiKey, bool.TrueString)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+ var context = new AuthorizationHandlerContext(_requirements, principal, null);
+
+ await _sut.HandleAsync(context);
+ Assert.True(context.HasSucceeded);
+ }
+
[Theory]
[MemberData(nameof(GetParts_ValidAuthHeader_Success_Data))]
public void GetParts_ValidAuthHeader_Success(string input, Dictionary<string, string> parts)
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
index ee42216e4..6669a6689 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
@@ -2,7 +2,8 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Api.Auth.FirstTimeSetupPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
@@ -11,25 +12,25 @@ using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
-namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
+namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
{
- public class FirstTimeSetupOrElevatedHandlerTests
+ public class FirstTimeSetupHandlerTests
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
- private readonly FirstTimeSetupOrElevatedHandler _sut;
+ private readonly FirstTimeSetupHandler _firstTimeSetupHandler;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
- public FirstTimeSetupOrElevatedHandlerTests()
+ public FirstTimeSetupHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
+ _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
- _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
+ _firstTimeSetupHandler = fixture.Create<FirstTimeSetupHandler>();
}
[Theory]
@@ -46,7 +47,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
var context = new AuthorizationHandlerContext(_requirements, claims, null);
- await _sut.HandleAsync(context);
+ await _firstTimeSetupHandler.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
var context = new AuthorizationHandlerContext(_requirements, claims, null);
- await _sut.HandleAsync(context);
+ await _firstTimeSetupHandler.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
index 7150c90bb..9cf8f8548 100644
--- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
- private readonly IgnoreParentalControlHandler _sut;
+ private readonly DefaultAuthorizationHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
@@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new IgnoreParentalControlRequirement() };
+ _requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement(validateParentalSchedule: false) };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
- _sut = fixture.Create<IgnoreParentalControlHandler>();
+ _sut = fixture.Create<DefaultAuthorizationHandler>();
}
[Theory]
diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
deleted file mode 100644
index 5b3d784ff..000000000
--- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System.Collections.Generic;
-using System.Net;
-using System.Threading.Tasks;
-using AutoFixture;
-using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.LocalAccessPolicy;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Moq;
-using Xunit;
-
-namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
-{
- public class LocalAccessHandlerTests
- {
- private readonly Mock<IConfigurationManager> _configurationManagerMock;
- private readonly List<IAuthorizationRequirement> _requirements;
- private readonly LocalAccessHandler _sut;
- private readonly Mock<IUserManager> _userManagerMock;
- private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
- private readonly Mock<INetworkManager> _networkManagerMock;
-
- public LocalAccessHandlerTests()
- {
- var fixture = new Fixture().Customize(new AutoMoqCustomization());
- _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new LocalAccessRequirement() };
- _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
- _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
- _networkManagerMock = fixture.Freeze<Mock<INetworkManager>>();
-
- _sut = fixture.Create<LocalAccessHandler>();
- }
-
- [Theory]
- [InlineData(true, true)]
- [InlineData(false, false)]
- public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
- {
- _networkManagerMock
- .Setup(n => n.IsInLocalNetwork(It.IsAny<IPAddress>()))
- .Returns(isInLocalNetwork);
-
- TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
- var claims = TestHelpers.SetupUser(
- _userManagerMock,
- _httpContextAccessor,
- UserRoles.User);
-
- var context = new AuthorizationHandlerContext(_requirements, claims, null);
- await _sut.HandleAsync(context);
- Assert.Equal(shouldSucceed, context.HasSucceeded);
- }
- }
-}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
deleted file mode 100644
index ffe88fcde..000000000
--- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using AutoFixture;
-using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.RequiresElevationPolicy;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Moq;
-using Xunit;
-
-namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
-{
- public class RequiresElevationHandlerTests
- {
- private readonly Mock<IConfigurationManager> _configurationManagerMock;
- private readonly List<IAuthorizationRequirement> _requirements;
- private readonly RequiresElevationHandler _sut;
- private readonly Mock<IUserManager> _userManagerMock;
- private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
-
- public RequiresElevationHandlerTests()
- {
- var fixture = new Fixture().Customize(new AutoMoqCustomization());
- _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
- _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
- _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
-
- _sut = fixture.Create<RequiresElevationHandler>();
- }
-
- [Theory]
- [InlineData(UserRoles.Administrator, true)]
- [InlineData(UserRoles.User, false)]
- [InlineData(UserRoles.Guest, false)]
- public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
- {
- TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
- var claims = TestHelpers.SetupUser(
- _userManagerMock,
- _httpContextAccessor,
- role);
-
- var context = new AuthorizationHandlerContext(_requirements, claims, null);
-
- await _sut.HandleAsync(context);
- Assert.Equal(shouldSucceed, context.HasSucceeded);
- }
- }
-}
diff --git a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs
new file mode 100644
index 000000000..d6428fb2c
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs
@@ -0,0 +1,36 @@
+using System;
+using Jellyfin.Api.Controllers;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Controllers;
+
+public static class ImageControllerTests
+{
+ [Theory]
+ [InlineData("image/apng", ".apng")]
+ [InlineData("image/avif", ".avif")]
+ [InlineData("image/bmp", ".bmp")]
+ [InlineData("image/gif", ".gif")]
+ [InlineData("image/x-icon", ".ico")]
+ [InlineData("image/jpeg", ".jpg")]
+ [InlineData("image/png", ".png")]
+ [InlineData("image/png; charset=utf-8", ".png")]
+ [InlineData("image/svg+xml", ".svg")]
+ [InlineData("image/tiff", ".tiff")]
+ [InlineData("image/webp", ".webp")]
+ public static void TryGetImageExtensionFromContentType_Valid_True(string contentType, string extension)
+ {
+ Assert.True(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
+ Assert.Equal(extension, ex);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("text/html")]
+ public static void TryGetImageExtensionFromContentType_InValid_False(string contentType)
+ {
+ Assert.False(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
+ Assert.Null(ex);
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
index c4640bd22..2d7741d81 100644
--- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
+++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
@@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Net;
using Xunit;
namespace Jellyfin.Api.Tests.Helpers
@@ -15,6 +19,82 @@ namespace Jellyfin.Api.Tests.Helpers
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
}
+ [Fact]
+ public static void GetUserId_IsAdmin()
+ {
+ Guid? requestUserId = Guid.NewGuid();
+ Guid? authUserId = Guid.NewGuid();
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.IsApiKey, bool.FalseString),
+ new Claim(ClaimTypes.Role, UserRoles.Administrator)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ var userId = RequestHelpers.GetUserId(principal, requestUserId);
+
+ Assert.Equal(requestUserId, userId);
+ }
+
+ [Fact]
+ public static void GetUserId_IsApiKey_EmptyGuid()
+ {
+ Guid? requestUserId = Guid.Empty;
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.IsApiKey, bool.TrueString)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ var userId = RequestHelpers.GetUserId(principal, requestUserId);
+
+ Assert.Equal(Guid.Empty, userId);
+ }
+
+ [Fact]
+ public static void GetUserId_IsApiKey_Null()
+ {
+ Guid? requestUserId = null;
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.IsApiKey, bool.TrueString)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ var userId = RequestHelpers.GetUserId(principal, requestUserId);
+
+ Assert.Equal(Guid.Empty, userId);
+ }
+
+ [Fact]
+ public static void GetUserId_IsUser()
+ {
+ Guid? requestUserId = Guid.NewGuid();
+ Guid? authUserId = Guid.NewGuid();
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.IsApiKey, bool.FalseString),
+ new Claim(ClaimTypes.Role, UserRoles.User)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ Assert.Throws<SecurityException>(() => RequestHelpers.GetUserId(principal, requestUserId));
+ }
+
public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData()
{
var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>();
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index c0e0d2b6b..015018910 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -5,37 +5,20 @@
<ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.17.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="AutoFixture.Xunit2" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
+ <PackageReference Include="Microsoft.Extensions.Options" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- <PackageReference Include="Moq" Version="4.18.4" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="coverlet.collector" />
+ <PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index c74127f04..8fef7fde0 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -5,32 +5,15 @@
<ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="coverlet.collector" />
+ <PackageReference Include="FsCheck.Xunit" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 1ddf5139c..54d93b48c 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -5,32 +5,15 @@
<ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index dc4b58fec..69677ce42 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -1,31 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs
new file mode 100644
index 000000000..c9018fe2f
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs
@@ -0,0 +1,47 @@
+using Emby.Dlna.Server;
+using MediaBrowser.Model.Dlna;
+using Xunit;
+
+namespace Jellyfin.Dlna.Server.Tests;
+
+public class DescriptionXmlBuilderTests
+{
+ [Fact]
+ public void GetFriendlyName_EmptyProfile_ReturnsServerName()
+ {
+ const string ServerName = "Test Server Name";
+ var builder = new DescriptionXmlBuilder(new DeviceProfile(), "serverUdn", "localhost", ServerName, string.Empty);
+ Assert.Equal(ServerName, builder.GetFriendlyName());
+ }
+
+ [Fact]
+ public void GetFriendlyName_FriendlyName_ReturnsFriendlyName()
+ {
+ const string FriendlyName = "Friendly Neighborhood Test Server";
+ var builder = new DescriptionXmlBuilder(
+ new DeviceProfile()
+ {
+ FriendlyName = FriendlyName
+ },
+ "serverUdn",
+ "localhost",
+ "Test Server Name",
+ string.Empty);
+ Assert.Equal(FriendlyName, builder.GetFriendlyName());
+ }
+
+ [Fact]
+ public void GetFriendlyName_FriendlyNameInterpolation_ReturnsFriendlyName()
+ {
+ var builder = new DescriptionXmlBuilder(
+ new DeviceProfile()
+ {
+ FriendlyName = "Friendly Neighborhood ${HostName}"
+ },
+ "serverUdn",
+ "localhost",
+ "Test Server Name",
+ string.Empty);
+ Assert.Equal("Friendly Neighborhood TestServerName", builder.GetFriendlyName());
+ }
+}
diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
index 2a7e8fafd..105e2a52a 100644
--- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
@@ -19,6 +19,11 @@ namespace Jellyfin.Extensions.Tests
[InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")]
[InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")]
[InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")]
+ [InlineData("a5", "a11")]
+ [InlineData("a05a", "a5b")]
+ [InlineData("a5a", "a05b")]
+ [InlineData("6xxx", "007asdf")]
+ [InlineData("00042Q", "42s")]
public void AlphanumericComparatorTest(params string?[] strings)
{
var copy = strings.Reverse().ToArray();
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 16b18cc85..036489829 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -1,34 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0">
+ <PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="FsCheck.Xunit" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs
new file mode 100644
index 000000000..be256da2e
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs
@@ -0,0 +1,37 @@
+using System.Text.Json;
+using Jellyfin.Extensions.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests.Json.Converters
+{
+ public class JsonBoolStringTests
+ {
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+ {
+ Converters =
+ {
+ new JsonBoolStringConverter()
+ }
+ };
+
+ [Theory]
+ [InlineData(@"{ ""Value"": ""true"" }", true)]
+ [InlineData(@"{ ""Value"": ""false"" }", false)]
+ public void Deserialize_String_Valid_Success(string input, bool output)
+ {
+ var s = JsonSerializer.Deserialize<TestStruct>(input, _jsonOptions);
+ Assert.Equal(s.Value, output);
+ }
+
+ [Theory]
+ [InlineData(true, "true")]
+ [InlineData(false, "false")]
+ public void Serialize_Bool_Success(bool input, string output)
+ {
+ var value = JsonSerializer.Serialize(input, _jsonOptions);
+ Assert.Equal(value, output);
+ }
+
+ private readonly record struct TestStruct(bool Value);
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
index c20f3dd99..eab003715 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
@@ -1,34 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0">
+ <PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
<ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Keyframes\Jellyfin.MediaEncoding.Keyframes.csproj" />
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
index 5cfad93a6..894bec6aa 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
@@ -1,36 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- <RootNamespace>Jellyfin.MediaEncoding.Keyframes</RootNamespace>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0">
+ <PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index 1b27e344b..db7e91c6a 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -17,6 +17,8 @@ namespace Jellyfin.MediaEncoding.Tests
}
[Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
+ [InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)]
@@ -36,6 +38,8 @@ namespace Jellyfin.MediaEncoding.Tests
{
public GetFFmpegVersionTestData()
{
+ Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
+ Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1));
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 02bf046ed..89ba42da0 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -2,6 +2,30 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
+ public const string FFmpegV60Output = @"ffmpeg version 6.0-Jellyfin Copyright (c) 2000-2023 the FFmpeg developers
+built with gcc 12.2.0 (crosstool-NG 1.25.0.90_cf9beb1)
+configuration: --prefix=/ffbuild/prefix --pkg-config=pkg-config --pkg-config-flags=--static --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --extra-version=Jellyfin --extra-cflags= --extra-cxxflags= --extra-ldflags= --extra-ldexeflags= --extra-libs= --enable-gpl --enable-version3 --enable-lto --disable-ffplay --disable-debug --disable-doc --disable-ptx-compression --disable-sdl2 --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-amf --enable-chromaprint --enable-libdav1d --enable-dxva2 --enable-d3d11va --enable-libfdk-aac --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-libvpl --enable-schannel --enable-libsrt --enable-libsvtav1 --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libzimg --enable-libzvbi
+libavutil 58. 2.100 / 58. 2.100
+libavcodec 60. 3.100 / 60. 3.100
+libavformat 60. 3.100 / 60. 3.100
+libavdevice 60. 1.100 / 60. 1.100
+libavfilter 9. 3.100 / 9. 3.100
+libswscale 7. 1.100 / 7. 1.100
+libswresample 4. 10.100 / 4. 10.100
+libpostproc 57. 1.100 / 57. 1.100";
+
+ public const string FFmpegV512Output = @"ffmpeg version 5.1.2-Jellyfin Copyright (c) 2000-2022 the FFmpeg developers
+built with gcc 10-win32 (GCC) 20220324
+configuration: --prefix=/opt/ffmpeg --arch=x86_64 --target-os=mingw32 --cross-prefix=x86_64-w64-mingw32- --pkg-config=pkg-config --pkg-config-flags=--static --extra-libs='-lfftw3f -lstdc++' --extra-cflags=-DCHROMAPRINT_NODLL --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --disable-w32threads --enable-pthreads --enable-shared --enable-lto --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
+libavutil 57. 28.100 / 57. 28.100
+libavcodec 59. 37.100 / 59. 37.100
+libavformat 59. 27.100 / 59. 27.100
+libavdevice 59. 7.100 / 59. 7.100
+libavfilter 8. 44.100 / 8. 44.100
+libswscale 6. 7.100 / 6. 7.100
+libswresample 4. 7.100 / 4. 7.100
+libpostproc 56. 6.100 / 56. 6.100";
+
public const string FFmpegV44Output = @"ffmpeg version 4.4-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 10.3.0 (Rev5, Built by MSYS2 project)
configuration: --disable-static --enable-shared --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls
diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
deleted file mode 100644
index 97dbb3be0..000000000
--- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.IO;
-using System.Text.Json;
-using System.Threading.Tasks;
-using Jellyfin.Extensions.Json;
-using MediaBrowser.MediaEncoding.Probing;
-using MediaBrowser.Model.IO;
-using Xunit;
-
-namespace Jellyfin.MediaEncoding.Tests
-{
- public class FFprobeParserTests
- {
- [Theory]
- [InlineData("ffprobe1.json")]
- public async Task Test(string fileName)
- {
- var path = Path.Join("Test Data", fileName);
- await using (var stream = AsyncFile.OpenRead(path))
- {
- var res = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
- Assert.NotNull(res);
- }
- }
- }
-}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index f824b6f3b..6b703e741 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -5,12 +5,6 @@
<ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -18,30 +12,19 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.17.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="AutoFixture.Xunit2" />
+ <PackageReference Include="coverlet.collector" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index bbe1246ca..6cb98b2b8 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -3,6 +3,7 @@ using System.Globalization;
using System.IO;
using System.Text.Json;
using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -15,9 +16,15 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
{
public class ProbeResultNormalizerTests
{
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private readonly JsonSerializerOptions _jsonOptions;
private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null);
+ public ProbeResultNormalizerTests()
+ {
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
+ _jsonOptions.Converters.Add(new JsonBoolStringConverter());
+ }
+
[Theory]
[InlineData("2997/125", 23.976f)]
[InlineData("1/50", 0.02f)]
@@ -31,16 +38,6 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
public void GetFrameRate_Success(string value, float? expected)
=> Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
- [Theory]
- [InlineData(0.5f, "0/1", false)]
- [InlineData(24.5f, "8/196", false)]
- [InlineData(63.5f, "1/127", true)]
- [InlineData(null, "1/60", false)]
- [InlineData(30f, "2/120", true)]
- [InlineData(59.999996f, "1563/187560", true)]
- public void IsCodecTimeBaseDoubleTheFrameRate_Success(float? frameRate, string codecTimeBase, bool expected)
- => Assert.Equal(expected, ProbeResultNormalizer.IsCodecTimeBaseDoubleTheFrameRate(frameRate, codecTimeBase));
-
[Fact]
public void GetMediaInfo_MetaData_Success()
{
@@ -159,6 +156,112 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
}
[Fact]
+ public void GetMediaInfo_TS_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_ts.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File);
+
+ Assert.Equal(2, res.MediaStreams.Count);
+
+ Assert.False(res.MediaStreams[0].IsAVC);
+ }
+
+ [Fact]
+ public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_progressive_no_field_order.json");
+
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_progressive_no_field_order.mp4", MediaProtocol.File);
+
+ Assert.Equal(2, res.MediaStreams.Count);
+
+ Assert.NotNull(res.VideoStream);
+ Assert.Equal(res.MediaStreams[0], res.VideoStream);
+ Assert.Equal(0, res.VideoStream.Index);
+ Assert.Equal("h264", res.VideoStream.Codec);
+ Assert.Equal("Main", res.VideoStream.Profile);
+ Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
+ Assert.Equal(1080, res.VideoStream.Height);
+ Assert.Equal(1920, res.VideoStream.Width);
+ Assert.False(res.VideoStream.IsInterlaced);
+ Assert.Equal("16:9", res.VideoStream.AspectRatio);
+ Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
+ Assert.Equal(41d, res.VideoStream.Level);
+ Assert.Equal(1, res.VideoStream.RefFrames);
+ Assert.True(res.VideoStream.IsAVC);
+ Assert.Equal(23.9760246f, res.VideoStream.RealFrameRate);
+ Assert.Equal("1/24000", res.VideoStream.TimeBase);
+ Assert.Equal(3948341, res.VideoStream.BitRate);
+ Assert.Equal(8, res.VideoStream.BitDepth);
+ Assert.True(res.VideoStream.IsDefault);
+ }
+
+ [Fact]
+ public void GetMediaInfo_ProgressiveVideoNoFieldOrder2_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_progressive_no_field_order2.json");
+
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_progressive_no_field_order2.mp4", MediaProtocol.File);
+
+ Assert.Single(res.MediaStreams);
+
+ Assert.NotNull(res.VideoStream);
+ Assert.Equal(res.MediaStreams[0], res.VideoStream);
+ Assert.Equal(0, res.VideoStream.Index);
+ Assert.Equal("h264", res.VideoStream.Codec);
+ Assert.Equal("High", res.VideoStream.Profile);
+ Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
+ Assert.Equal(720, res.VideoStream.Height);
+ Assert.Equal(1280, res.VideoStream.Width);
+ Assert.False(res.VideoStream.IsInterlaced);
+ Assert.Equal("16:9", res.VideoStream.AspectRatio);
+ Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
+ Assert.Equal(31d, res.VideoStream.Level);
+ Assert.Equal(1, res.VideoStream.RefFrames);
+ Assert.True(res.VideoStream.IsAVC);
+ Assert.Equal(25f, res.VideoStream.RealFrameRate);
+ Assert.Equal("1/12800", res.VideoStream.TimeBase);
+ Assert.Equal(53288, res.VideoStream.BitRate);
+ Assert.Equal(8, res.VideoStream.BitDepth);
+ Assert.True(res.VideoStream.IsDefault);
+ }
+
+ [Fact]
+ public void GetMediaInfo_InterlacedVideo_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_interlaced.json");
+
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_interlaced.mp4", MediaProtocol.File);
+
+ Assert.Single(res.MediaStreams);
+
+ Assert.NotNull(res.VideoStream);
+ Assert.Equal(res.MediaStreams[0], res.VideoStream);
+ Assert.Equal(0, res.VideoStream.Index);
+ Assert.Equal("h264", res.VideoStream.Codec);
+ Assert.Equal("High", res.VideoStream.Profile);
+ Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
+ Assert.Equal(720, res.VideoStream.Height);
+ Assert.Equal(1280, res.VideoStream.Width);
+ Assert.True(res.VideoStream.IsInterlaced);
+ Assert.Equal("16:9", res.VideoStream.AspectRatio);
+ Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
+ Assert.Equal(40d, res.VideoStream.Level);
+ Assert.Equal(1, res.VideoStream.RefFrames);
+ Assert.True(res.VideoStream.IsAVC);
+ Assert.Equal(25f, res.VideoStream.RealFrameRate);
+ Assert.Equal("1/12800", res.VideoStream.TimeBase);
+ Assert.Equal(56945, res.VideoStream.BitRate);
+ Assert.Equal(8, res.VideoStream.BitDepth);
+ Assert.True(res.VideoStream.IsDefault);
+ }
+
+ [Fact]
public void GetMediaInfo_MusicVideo_Success()
{
var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json");
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json
new file mode 100644
index 000000000..810244920
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json
@@ -0,0 +1,81 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High",
+ "codec_type": "video",
+ "codec_tag_string": "avc1",
+ "codec_tag": "0x31637661",
+ "width": 1280,
+ "height": 720,
+ "coded_width": 1280,
+ "coded_height": 720,
+ "closed_captions": 0,
+ "film_grain": 0,
+ "has_b_frames": 2,
+ "pix_fmt": "yuv420p",
+ "level": 40,
+ "chroma_location": "left",
+ "field_order": "tt",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "id": "0x1",
+ "r_frame_rate": "25/1",
+ "avg_frame_rate": "25/1",
+ "time_base": "1/12800",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 3840000,
+ "duration": "300.000000",
+ "bit_rate": "56945",
+ "bits_per_raw_sample": "8",
+ "nb_frames": "7500",
+ "extradata_size": 42,
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "und",
+ "handler_name": "VideoHandler",
+ "vendor_id": "[0][0][0][0]"
+ }
+ }
+ ],
+ "format": {
+ "filename": "test-gray.720i.mp4",
+ "nb_streams": 1,
+ "nb_programs": 0,
+ "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
+ "format_long_name": "QuickTime / MOV",
+ "start_time": "0.000000",
+ "duration": "300.000000",
+ "size": "2223957",
+ "bit_rate": "59305",
+ "probe_score": 100,
+ "tags": {
+ "major_brand": "isom",
+ "minor_version": "512",
+ "compatible_brands": "isomiso2avc1mp41",
+ "encoder": "Lavf58.20.100"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json
new file mode 100644
index 000000000..897c5e3ab
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json
@@ -0,0 +1,133 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "Main",
+ "codec_type": "video",
+ "codec_time_base": "1001/48000",
+ "codec_tag_string": "avc1",
+ "codec_tag": "0x31637661",
+ "width": 1920,
+ "height": 1080,
+ "coded_width": 1920,
+ "coded_height": 1088,
+ "closed_captions": 0,
+ "has_b_frames": 1,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "16:9",
+ "pix_fmt": "yuv420p",
+ "level": 41,
+ "chroma_location": "left",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "r_frame_rate": "24000/1001",
+ "avg_frame_rate": "24000/1001",
+ "time_base": "1/24000",
+ "start_pts": 1000,
+ "start_time": "0.041667",
+ "duration_ts": 29095066,
+ "duration": "1212.294417",
+ "bit_rate": "3948341",
+ "bits_per_raw_sample": "8",
+ "nb_frames": "29066",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2020-01-20T13:56:34.000000Z",
+ "language": "eng",
+ "handler_name": "\fVideoHandler",
+ "encoder": "h264"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "ac3",
+ "codec_long_name": "ATSC A/52A (AC-3)",
+ "codec_type": "audio",
+ "codec_time_base": "1/48000",
+ "codec_tag_string": "ac-3",
+ "codec_tag": "0x332d6361",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "dmix_mode": "-1",
+ "ltrt_cmixlev": "-1.000000",
+ "ltrt_surmixlev": "-1.000000",
+ "loro_cmixlev": "-1.000000",
+ "loro_surmixlev": "-1.000000",
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/48000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 58232832,
+ "duration": "1213.184000",
+ "bit_rate": "224000",
+ "nb_frames": "37912",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2020-01-20T13:56:34.000000Z",
+ "language": "eng",
+ "handler_name": "\fSoundHandler"
+ },
+ "side_data_list": [
+ {
+ "side_data_type": "Audio Service Type"
+ }
+ ]
+ }
+ ],
+ "format": {
+ "filename": "The Big Bang Theory - S01E17.mp4",
+ "nb_streams": 2,
+ "nb_programs": 0,
+ "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
+ "format_long_name": "QuickTime / MOV",
+ "start_time": "0.000000",
+ "duration": "1213.184000",
+ "size": "633084606",
+ "bit_rate": "4174698",
+ "probe_score": 100,
+ "tags": {
+ "major_brand": "mp42",
+ "minor_version": "512",
+ "compatible_brands": "mp42",
+ "creation_time": "2020-01-20T13:56:34.000000Z",
+ "media_type": "9",
+ "season_number": "0",
+ "episode_sort": "0",
+ "hd_video": "0",
+ "iTunMOVI": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>studio</key><string>studio</string><key>cast</key><array><dict><key>name</key><string></string></dict></array><key>directors</key><array><dict><key>name</key><string></string></dict></array><key>producers</key><array><dict><key>name</key><string></string></dict></array><key>codirectors</key><array><dict><key>name</key><string>codirector</string></dict></array><key>screenwriters</key><array><dict><key>name</key><string></string></dict></array></dict></plist>"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json
new file mode 100644
index 000000000..4a03e0d61
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json
@@ -0,0 +1,72 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High",
+ "codec_type": "video",
+ "codec_time_base": "1/50",
+ "codec_tag_string": "avc1",
+ "codec_tag": "0x31637661",
+ "width": 1280,
+ "height": 720,
+ "coded_width": 1280,
+ "coded_height": 720,
+ "closed_captions": 0,
+ "has_b_frames": 2,
+ "pix_fmt": "yuv420p",
+ "level": 31,
+ "chroma_location": "left",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "r_frame_rate": "25/1",
+ "avg_frame_rate": "25/1",
+ "time_base": "1/12800",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 3840000,
+ "duration": "300.000000",
+ "bit_rate": "53288",
+ "bits_per_raw_sample": "8",
+ "nb_frames": "7500",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "language": "und",
+ "handler_name": "VideoHandler"
+ }
+ }
+ ],
+ "format": {
+ "filename": "test-gray.720p.mp4",
+ "nb_streams": 1,
+ "nb_programs": 0,
+ "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
+ "format_long_name": "QuickTime / MOV",
+ "start_time": "0.000000",
+ "duration": "300.000000",
+ "size": "2086818",
+ "bit_rate": "55648",
+ "probe_score": 100,
+ "tags": {
+ "major_brand": "isom",
+ "minor_version": "512",
+ "compatible_brands": "isomiso2avc1mp41",
+ "encoder": "Lavf58.20.100"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json
index cdad5df50..cdad5df50 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 60be17a74..f05a0152e 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -26,7 +26,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
[InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
[InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
[InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -37,7 +37,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
[InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
[InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
[InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -88,7 +88,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
[InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
- [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
+ [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")]
[InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
[InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -176,7 +176,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
[InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
[InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -186,7 +186,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
[InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
[InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -275,10 +275,10 @@ namespace Jellyfin.Model.Tests
// Chrome
[InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
[InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
- [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
// Firefox
[InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
// Yatse
[InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
[InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index b6578a7f1..8345b610e 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -1,21 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
+ <PackageReference Include="coverlet.collector" />
+ <PackageReference Include="FsCheck.Xunit" />
</ItemGroup>
<ItemGroup>
@@ -24,17 +18,6 @@
</None>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
index cbab455f0..371c3811a 100644
--- a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
+++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
@@ -127,9 +127,10 @@ namespace Jellyfin.Model.Tests.Net
[InlineData("image/jpeg", ".jpg")]
[InlineData("image/png", ".png")]
[InlineData("image/svg+xml", ".svg")]
- [InlineData("image/tiff", ".tif")]
+ [InlineData("image/tiff", ".tiff")]
[InlineData("image/vnd.microsoft.icon", ".ico")]
[InlineData("image/webp", ".webp")]
+ [InlineData("image/x-icon", ".ico")]
[InlineData("image/x-png", ".png")]
[InlineData("text/css", ".css")]
[InlineData("text/csv", ".csv")]
diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
index 58aaed023..c49663248 100644
--- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
+++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
@@ -12,8 +12,6 @@ namespace Jellyfin.Naming.Tests.Common
Assert.NotEmpty(options.CleanDateTimeRegexes);
Assert.NotEmpty(options.CleanStringRegexes);
- Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);
- Assert.NotEmpty(options.EpisodeMultiPartRegexes);
}
[Fact]
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index f10f9159d..112dd780e 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -5,36 +5,19 @@
<ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
+ <PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
index 68059f980..406381f14 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
@@ -73,6 +73,11 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number
[InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number
[InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
+ [InlineData("Season 3/The Series Season 3 Episode 9 - The title.avi", 9)]
+ [InlineData("Season 3/The Series S3 E9 - The title.avi", 9)]
+ [InlineData("Season 3/S003 E009.avi", 9)]
+ [InlineData("Season 3/Season 3 Episode 9.avi", 9)]
+
// [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
// TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
index af219b118..7604ddc80 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
@@ -30,6 +30,7 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)]
[InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)]
+ [InlineData("/The.Sopranos/Season 3/The Sopranos Season 3 Episode 09 - The Telltale Moozadell.avi", false, "The Sopranos", 3, 9)]
// TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)]
// TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)]
// TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)]
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 287d881a8..294f11ee7 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -188,8 +188,7 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man-bluray.mkv",
@"/movies/Iron Man/Iron Man-3d.mkv",
@"/movies/Iron Man/Iron Man-3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man-3d.hsbs.mkv",
- @"/movies/Iron Man/Iron Man[test].mkv",
+ @"/movies/Iron Man/Iron Man[test].mkv"
};
var result = VideoListResolver.Resolve(
@@ -197,10 +196,14 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Equal(7, result[0].AlternateVersions.Count);
- Assert.False(result[0].AlternateVersions[2].Is3D);
- Assert.True(result[0].AlternateVersions[3].Is3D);
- Assert.True(result[0].AlternateVersions[4].Is3D);
+ Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path);
+ Assert.Equal(6, result[0].AlternateVersions.Count);
+ Assert.Equal("/movies/Iron Man/Iron Man-720p.mkv", result[0].AlternateVersions[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-3d.mkv", result[0].AlternateVersions[1].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-3d-hsbs.mkv", result[0].AlternateVersions[2].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-bluray.mkv", result[0].AlternateVersions[3].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-test.mkv", result[0].AlternateVersions[4].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man[test].mkv", result[0].AlternateVersions[5].Path);
}
[Fact]
@@ -214,7 +217,6 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man - bluray.mkv",
@"/movies/Iron Man/Iron Man - 3d.mkv",
@"/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man - 3d.hsbs.mkv",
@"/movies/Iron Man/Iron Man [test].mkv"
};
@@ -223,10 +225,14 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Equal(7, result[0].AlternateVersions.Count);
- Assert.False(result[0].AlternateVersions[3].Is3D);
- Assert.True(result[0].AlternateVersions[4].Is3D);
- Assert.True(result[0].AlternateVersions[5].Is3D);
+ Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path);
+ Assert.Equal(6, result[0].AlternateVersions.Count);
+ Assert.Equal("/movies/Iron Man/Iron Man - 720p.mkv", result[0].AlternateVersions[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - 3d.mkv", result[0].AlternateVersions[1].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - 3d-hsbs.mkv", result[0].AlternateVersions[2].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - bluray.mkv", result[0].AlternateVersions[3].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - test.mkv", result[0].AlternateVersions[4].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man [test].mkv", result[0].AlternateVersions[5].Path);
}
[Fact]
@@ -324,6 +330,33 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
+ public void TestMultiVersion12()
+ {
+ var files = new[]
+ {
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv",
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv",
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv",
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv",
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ _namingOptions).ToList();
+
+ Assert.Single(result);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
+ Assert.Equal(5, result[0].AlternateVersions.Count);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[1].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[2].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[3].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[4].Path);
+ }
+
+ [Fact]
public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
{
var files = new[]
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 3a39daa36..4b4bdd2a5 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -5,33 +5,16 @@
<ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
- <PackageReference Include="Moq" Version="4.18.4" />
- </ItemGroup>
-
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="coverlet.collector" />
+ <PackageReference Include="FsCheck.Xunit" />
+ <PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
index 61f913252..df2a2ca70 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
@@ -45,6 +45,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("fd23:184f:2029:0::/56", "fd24:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d500/120", "fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0::/56", "192.168.10.60")]
+ [InlineData("2001:abcd:abcd:6b40::0/60", "192.168.10.60")]
public void InNetwork_False_Success(string network, string value)
{
var ip = IPAddress.Parse(value);
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index 6cc998d27..c12f0cd68 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -13,30 +7,19 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0">
+ <PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 82628d733..9b6cb40b0 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -5,13 +5,6 @@
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -19,28 +12,17 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.17.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="Xunit.SkippableFact" />
+ <PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs
new file mode 100644
index 000000000..d136c1bc6
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs
@@ -0,0 +1,76 @@
+using System.Linq;
+using Emby.Naming.Common;
+using Emby.Server.Implementations.Library.Resolvers.Audio;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class AudioResolverTests
+{
+ private static readonly NamingOptions _namingOptions = new();
+
+ [Theory]
+ [InlineData("words.mp3")] // single non-tagged file
+ [InlineData("chapter 01.mp3")]
+ [InlineData("part 1.mp3")]
+ [InlineData("chapter 01.mp3", "non-media.txt")]
+ [InlineData("title.mp3", "title.epub")]
+ [InlineData("01.mp3", "subdirectory/")] // single media file with sub-directory - note that this will hide any contents in the subdirectory
+ public void Resolve_AudiobookDirectory_SingleResult(params string[] children)
+ {
+ var resolved = TestResolveChildren("/parent/title", children);
+ Assert.NotNull(resolved);
+ }
+
+ [Theory]
+ /* Results that can't be displayed as an audio book. */
+ [InlineData] // no contents
+ [InlineData("subdirectory/")]
+ [InlineData("non-media.txt")]
+ /* Names don't indicate parts of a single book. */
+ [InlineData("Name.mp3", "Another Name.mp3")]
+ /* Results that are an audio book but not currently navigable as such (multiple chapters and/or parts). */
+ [InlineData("01.mp3", "02.mp3")]
+ [InlineData("chapter 01.mp3", "chapter 02.mp3")]
+ [InlineData("part 1.mp3", "part 2.mp3")]
+ [InlineData("chapter 01 part 01.mp3", "chapter 01 part 02.mp3")]
+ /* Mismatched chapters, parts, and named files. */
+ [InlineData("chapter 01.mp3", "part 2.mp3")]
+ [InlineData("book title.mp3", "chapter name.mp3")] // "book title" resolves as alternate version of book based on directory name
+ [InlineData("01 Content.mp3", "01 Credits.mp3")] // resolves as alternate versions of chapter 1
+ [InlineData("Chapter Name.mp3", "Part 1.mp3")]
+ public void Resolve_AudiobookDirectory_NoResult(params string[] children)
+ {
+ var resolved = TestResolveChildren("/parent/book title", children);
+ Assert.Null(resolved);
+ }
+
+ private Audio? TestResolveChildren(string parent, string[] children)
+ {
+ var childrenMetadata = children.Select(name => new FileSystemMetadata
+ {
+ FullName = parent + "/" + name,
+ IsDirectory = name.EndsWith('/')
+ }).ToArray();
+
+ var resolver = new AudioResolver(_namingOptions);
+ var itemResolveArgs = new ItemResolveArgs(
+ null,
+ Mock.Of<ILibraryManager>())
+ {
+ CollectionType = "books",
+ FileInfo = new FileSystemMetadata
+ {
+ FullName = parent,
+ IsDirectory = true
+ },
+ FileSystemChildren = childrenMetadata
+ };
+
+ return resolver.Resolve(itemResolveArgs);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
index 286ba0405..6d0ed7bbb 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -22,10 +22,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
var parent = new Folder { Name = "extras" };
- var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
+ var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
- Mock.Of<IDirectoryService>())
+ null)
{
Parent = parent,
CollectionType = CollectionType.TvShows,
@@ -45,10 +45,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
// Have to create a mock because of moq proxies not being castable to a concrete implementation
// https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
- var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
+ var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
- Mock.Of<IDirectoryService>())
+ null)
{
Parent = series,
CollectionType = CollectionType.TvShows,
@@ -62,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private sealed class EpisodeResolverMock : EpisodeResolver
{
- public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions)
+ public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService)
{
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
index efc3ac0c2..aed584355 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
@@ -18,10 +18,10 @@ public class MovieResolverTests
[Fact]
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
{
- var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions);
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
- Mock.Of<IDirectoryService>())
+ null)
{
Parent = null,
FileInfo = new FileSystemMetadata
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
index 82ce8fc4e..92b4178fd 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
@@ -67,4 +67,23 @@ public class XmlTvListingsProviderTests
Assert.Equal("https://domain.tld/image.png", program.ImageUrl);
Assert.Equal("3297", program.ChannelId);
}
+
+ [Theory]
+ [InlineData("Test Data/LiveTv/Listings/XmlTv/emptycategory.xml")]
+ [InlineData("https://example.com/emptycategory.xml")]
+ public async Task GetProgramsAsync_EmptyCategories_Success(string path)
+ {
+ var info = new ListingsProviderInfo()
+ {
+ Path = path
+ };
+
+ var startDate = new DateTime(2022, 11, 4);
+ var programs = await _xmlTvListingsProvider.GetProgramsAsync(info, "3297", startDate, startDate.AddDays(1), CancellationToken.None);
+ var programsList = programs.ToList();
+ Assert.Single(programsList);
+ var program = programsList[0];
+ Assert.DoesNotContain(program.Genres, g => string.IsNullOrEmpty(g));
+ Assert.Equal("3297", program.ChannelId);
+ }
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 16eb7a75c..7fabe9904 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -83,11 +83,11 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(23, ratings.Count);
+ Assert.Equal(54, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
- Assert.Equal(9, tvma!.Value);
+ Assert.Equal(17, tvma!.Value);
}
[Fact]
@@ -100,21 +100,21 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(10, ratings.Count);
+ Assert.Equal(19, ratings.Count);
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);
- Assert.Equal(7, fsk!.Value);
+ Assert.Equal(12, fsk!.Value);
}
[Theory]
- [InlineData("CA-R", "CA", 10)]
- [InlineData("FSK-16", "DE", 8)]
- [InlineData("FSK-18", "DE", 9)]
- [InlineData("FSK-18", "US", 9)]
- [InlineData("TV-MA", "US", 9)]
- [InlineData("XXX", "asdf", 100)]
- [InlineData("Germany: FSK-18", "DE", 9)]
+ [InlineData("CA-R", "CA", 18)]
+ [InlineData("FSK-16", "DE", 16)]
+ [InlineData("FSK-18", "DE", 18)]
+ [InlineData("FSK-18", "US", 18)]
+ [InlineData("TV-MA", "US", 17)]
+ [InlineData("XXX", "asdf", 1000)]
+ [InlineData("Germany: FSK-18", "DE", 18)]
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
{
var localizationManager = Setup(new ServerConfiguration()
@@ -135,6 +135,9 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
UICulture = "de-DE"
});
await localizationManager.LoadAll();
+ Assert.Null(localizationManager.GetRatingLevel("NR"));
+ Assert.Null(localizationManager.GetRatingLevel("unrated"));
+ Assert.Null(localizationManager.GetRatingLevel("Not Rated"));
Assert.Null(localizationManager.GetRatingLevel("n/a"));
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
new file mode 100644
index 000000000..dd4aa8977
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
@@ -0,0 +1,6 @@
+<tv date="20221104">
+ <programme channel="3297" start="20221104130000 -0400" stop="20221105235959 -0400">
+ <category lang="en" />
+ <category lang="en">sports</category>
+ </programme>
+</tv>
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index 9eb0beda4..3737fee0a 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Models.StartupDtos;
using Jellyfin.Api.Models.UserDtos;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
using Xunit;
namespace Jellyfin.Server.Integration.Tests
@@ -43,6 +44,33 @@ namespace Jellyfin.Server.Integration.Tests
return auth!.AccessToken;
}
+ public static async Task<UserDto> GetUserDtoAsync(HttpClient client)
+ {
+ using var response = await client.GetAsync("Users/Me").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var userDto = await JsonSerializer.DeserializeAsync<UserDto>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false);
+ Assert.NotNull(userDto);
+ return userDto;
+ }
+
+ public static async Task<BaseItemDto> GetRootFolderDtoAsync(HttpClient client, Guid userId = default)
+ {
+ if (userId.Equals(default))
+ {
+ var userDto = await GetUserDtoAsync(client).ConfigureAwait(false);
+ userId = userDto.Id;
+ }
+
+ var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ JsonDefaults.Options).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ return rootDto;
+ }
+
public static void AddAuthHeader(this HttpHeaders headers, string accessToken)
{
headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
new file mode 100644
index 000000000..078002994
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public ItemsControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetItems_NoApiKeyOrUserId_Success()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("Items").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items")]
+ [InlineData("Users/{0}/Items/Resume")]
+ public async Task GetUserItems_NonExistentUserId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("Items?userId={0}")]
+ [InlineData("Users/{0}/Items")]
+ [InlineData("Users/{0}/Items/Resume")]
+ public async Task GetItems_UserId_Ok(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(items);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
new file mode 100644
index 000000000..013d19a9f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public LibraryControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Theory]
+ [InlineData("Items/{0}/File")]
+ [InlineData("Items/{0}/ThemeSongs")]
+ [InlineData("Items/{0}/ThemeVideos")]
+ [InlineData("Items/{0}/ThemeMedia")]
+ [InlineData("Items/{0}/Ancestors")]
+ [InlineData("Items/{0}/Download")]
+ [InlineData("Artists/{0}/Similar")]
+ [InlineData("Items/{0}/Similar")]
+ [InlineData("Albums/{0}/Similar")]
+ [InlineData("Shows/{0}/Similar")]
+ [InlineData("Movies/{0}/Similar")]
+ [InlineData("Trailers/{0}/Similar")]
+ public async Task Get_NonExistentItemId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
new file mode 100644
index 000000000..17f3dc99f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
@@ -0,0 +1,26 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public MusicGenreControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task MusicGenres_FakeMusicGenre_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
new file mode 100644
index 000000000..868ecd53f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public PlaystateControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMarkPlayedItem_NonExistentUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMarkPlayedItem_NonExistentItemId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
new file mode 100644
index 000000000..cb0a829e8
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public SessionControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetSessions_NonExistentUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
index 2b825a93a..2a3c53dbe 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -67,6 +67,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
}
[Fact]
+ [Priority(-1)]
+ public async Task Me_Valid_Success()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ }
+
+ [Fact]
[Priority(0)]
public async Task New_Valid_Success()
{
@@ -108,7 +118,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
var createRequest = new CreateUserByName()
{
- Name = username
+ Name = username!
};
using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
@@ -116,6 +126,19 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
}
[Fact]
+ [Priority(0)]
+ public async Task Delete_DoesntExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+
+ // access token can't be null here as the previous test populated it
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
+
+ using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
[Priority(1)]
public async Task UpdateUserPassword_Valid_Success()
{
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
new file mode 100644
index 000000000..69f2ccf33
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public UserLibraryControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetRootFolder_NonExistenUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetRootFolder_UserId_Valid()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items/{1}")]
+ [InlineData("Users/{0}/Items/{1}/Intros")]
+ [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+ [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+ [InlineData("Users/{0}/Items/{1}/Lyrics")]
+ public async Task GetItem_NonExistenUserId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items/{1}")]
+ [InlineData("Users/{0}/Items/{1}/Intros")]
+ [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+ [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+ [InlineData("Users/{0}/Items/{1}/Lyrics")]
+ public async Task GetItem_NonExistentItemId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetItem_UserIdAndItemId_Valid()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+ var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ }
+
+ [Fact]
+ public async Task GetIntros_UserIdAndItemId_Valid()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+ var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+ [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+ public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
new file mode 100644
index 000000000..0f9a2e90a
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public VideosControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task DeleteAlternateSources_NonExistentItemId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 006b38a11..a5296d8c9 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -1,25 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.17.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="AutoFixture.Xunit2" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
+ <PackageReference Include="Microsoft.Extensions.Options" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Xunit.Priority" Version="1.1.6" />
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- <PackageReference Include="Moq" Version="4.18.4" />
+ <PackageReference Include="Xunit.Priority" />
+ <PackageReference Include="coverlet.collector" />
+ <PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
@@ -29,17 +24,6 @@
</None>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 1bfa5996d..55bc43455 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -4,6 +4,8 @@ using System.Globalization;
using System.IO;
using System.Threading;
using Emby.Server.Implementations;
+using Jellyfin.Server.Extensions;
+using Jellyfin.Server.Helpers;
using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
@@ -11,6 +13,7 @@ using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
@@ -33,7 +36,7 @@ namespace Jellyfin.Server.Integration.Tests
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(formatProvider: CultureInfo.InvariantCulture)
.CreateLogger();
- Program.PerformStaticInitialization();
+ StartupHelpers.PerformStaticInitialization();
}
/// <inheritdoc/>
@@ -63,7 +66,7 @@ namespace Jellyfin.Server.Integration.Tests
// Create the logging config file
// TODO: We shouldn't need to do this since we are only logging to console
- Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult();
+ StartupHelpers.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult();
// Create a copy of the application configuration to use for startup
var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
@@ -81,7 +84,7 @@ namespace Jellyfin.Server.Integration.Tests
_disposableComponents.Add(appHost);
builder.ConfigureServices(services => appHost.Init(services))
- .ConfigureWebHostBuilder(appHost, startupConfig, appPaths)
+ .ConfigureWebHostBuilder(appHost, startupConfig, appPaths, NullLogger.Instance)
.ConfigureAppConfiguration((context, builder) =>
{
builder
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 771fad635..5fea805ae 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -1,36 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.17.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="AutoFixture.Xunit2" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
+ <PackageReference Include="Microsoft.Extensions.Options" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- <PackageReference Include="Moq" Version="4.18.4" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="coverlet.collector" />
+ <PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
index d15c9d6f5..797fc8f64 100644
--- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Server.Middleware;
+using Jellyfin.Api.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index 0d69c3f61..9fe0744de 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -13,25 +7,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
- <PackageReference Include="Moq" Version="4.18.4" />
- <PackageReference Include="xunit" Version="2.4.2" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.2.0" />
- </ItemGroup>
-
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ <PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>