aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2020-08-20 16:40:03 +0200
committerGitHub <noreply@github.com>2020-08-20 16:40:03 +0200
commit5160e627f18fb4a763eaa77b836d20486e55c5e9 (patch)
tree5fb90ba0ee4d217384d31d1828b6a42a74168a45
parent3588ee5229b76bca9417813e208e86492e06d609 (diff)
parent250e351613e0eed7977c8cdad4a9078927458feb (diff)
Merge branch 'master' into feature/ffmpeg-version-check
-rw-r--r--.ci/azure-pipelines-abi.yml28
-rw-r--r--.ci/azure-pipelines-package.yml51
-rw-r--r--.ci/azure-pipelines.yml4
-rw-r--r--.vscode/launch.json14
-rw-r--r--Emby.Dlna/Api/DlnaServerService.cs383
-rw-r--r--Emby.Dlna/Api/DlnaService.cs88
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs1
-rw-r--r--Emby.Dlna/ControlResponse.cs6
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs3
-rw-r--r--Emby.Dlna/DlnaManager.cs36
-rw-r--r--Emby.Dlna/Eventing/EventManager.cs14
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs6
-rw-r--r--Emby.Dlna/PlayTo/Device.cs40
-rw-r--r--Emby.Dlna/PlayTo/TransportCommands.cs17
-rw-r--r--Emby.Dlna/Profiles/DefaultProfile.cs2
-rw-r--r--Emby.Dlna/Profiles/DenonAvrProfile.cs2
-rw-r--r--Emby.Dlna/Profiles/DirectTvProfile.cs2
-rw-r--r--Emby.Dlna/Profiles/Foobar2000Profile.cs2
-rw-r--r--Emby.Dlna/Profiles/MarantzProfile.cs2
-rw-r--r--Emby.Dlna/Profiles/MediaMonkeyProfile.cs3
-rw-r--r--Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs3
-rw-r--r--Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs3
-rw-r--r--Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs3
-rw-r--r--Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs3
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs182
-rw-r--r--Emby.Dlna/Service/ServiceXmlBuilder.cs34
-rw-r--r--Emby.Drawing/ImageProcessor.cs14
-rw-r--r--Emby.Naming/Common/NamingOptions.cs60
-rw-r--r--Emby.Naming/TV/SeasonPathParser.cs2
-rw-r--r--Emby.Notifications/Api/NotificationsService.cs191
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs16
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs54
-rw-r--r--Emby.Server.Implementations/Browser/BrowserLauncher.cs4
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs24
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs225
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs249
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs23
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs48
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj20
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs93
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs13
-rw-r--r--Emby.Server.Implementations/HttpServer/FileWriter.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs6
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpResultFactory.cs5
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs38
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs13
-rw-r--r--Emby.Server.Implementations/HttpServer/StreamWriter.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs8
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs3
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs10
-rw-r--r--Emby.Server.Implementations/Images/ArtistImageProvider.cs10
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs24
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs23
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs282
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs24
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs48
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs2
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs20
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs115
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs36
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs88
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs25
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json38
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json56
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json50
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json64
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json24
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json107
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs8
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs43
-rw-r--r--Emby.Server.Implementations/Networking/NetworkManager.cs17
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs64
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs7
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs8
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs10
-rw-r--r--Emby.Server.Implementations/Services/ServiceController.cs19
-rw-r--r--Emby.Server.Implementations/Services/ServiceExec.cs9
-rw-r--r--Emby.Server.Implementations/Services/ServiceHandler.cs26
-rw-r--r--Emby.Server.Implementations/Services/ServicePath.cs8
-rw-r--r--Emby.Server.Implementations/Services/SwaggerService.cs287
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs6
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs30
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs4
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs92
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs10
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs29
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs5
-rw-r--r--Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs35
-rw-r--r--Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs35
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs103
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs15
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs11
-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/FirstTimeSetupOrElevatedHandler.cs22
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs45
-rw-r--r--Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs11
-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.cs26
-rw-r--r--Jellyfin.Api/Constants/InternalClaimTypes.cs38
-rw-r--r--Jellyfin.Api/Constants/Policies.cs37
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs57
-rw-r--r--Jellyfin.Api/Controllers/AlbumsController.cs134
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs97
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs488
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs198
-rw-r--r--Jellyfin.Api/Controllers/BrandingController.cs57
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs256
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs111
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs126
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs273
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs155
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs176
-rw-r--r--Jellyfin.Api/Controllers/DlnaController.cs132
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs257
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs1789
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs191
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs218
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs320
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs154
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs231
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs1304
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs330
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs364
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs85
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs (renamed from MediaBrowser.Api/ItemUpdateService.cs)426
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs593
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs1036
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs331
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs1238
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs76
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs315
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs330
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs313
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs145
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs151
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs285
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs199
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs367
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs200
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs260
-rw-r--r--Jellyfin.Api/Controllers/ScheduledTasksController.cs161
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs269
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs491
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs65
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs277
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs350
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs88
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs205
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs219
-rw-r--r--Jellyfin.Api/Controllers/TimeSyncController.cs39
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs281
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs385
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs330
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs541
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs392
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs149
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs81
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs503
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs527
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs234
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs162
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs195
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs75
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs550
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs137
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs (renamed from MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs)13
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs95
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs (renamed from MediaBrowser.Api/Playback/MediaInfoService.cs)522
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs175
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs181
-rw-r--r--Jellyfin.Api/Helpers/SimilarItemsHelper.cs (renamed from MediaBrowser.Api/SimilarItemsHelper.cs)102
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs758
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs886
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj5
-rw-r--r--Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs18
-rw-r--r--Jellyfin.Api/Models/ConfigurationPageInfo.cs (renamed from MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs)38
-rw-r--r--Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs106
-rw-r--r--Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs13
-rw-r--r--Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs23
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs18
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs34
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs41
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs19
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs15
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs27
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs36
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs166
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs24
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs51
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs21
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs20
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs (renamed from MediaBrowser.Api/TranscodingJob.cs)136
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs (renamed from MediaBrowser.Api/Playback/TranscodingThrottler.cs)125
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs30
-rw-r--r--Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs40
-rw-r--r--Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs18
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs8
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs6
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs13
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs13
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamState.cs201
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs45
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs19
-rw-r--r--Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs23
-rw-r--r--Jellyfin.Api/Models/UserDtos/CreateUserByName.cs18
-rw-r--r--Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs23
-rw-r--r--Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs28
-rw-r--r--Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs18
-rw-r--r--Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs15
-rw-r--r--Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs44
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs (renamed from MediaBrowser.Api/System/ActivityLogWebSocketListener.cs)30
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs (renamed from MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs)65
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs (renamed from MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs)62
-rw-r--r--Jellyfin.Data/Entities/DisplayPreferences.cs150
-rw-r--r--Jellyfin.Data/Entities/HomeSection.cs46
-rw-r--r--Jellyfin.Data/Entities/ItemDisplayPreferences.cs120
-rw-r--r--Jellyfin.Data/Entities/User.cs15
-rw-r--r--Jellyfin.Data/Enums/ChromecastVersion.cs18
-rw-r--r--Jellyfin.Data/Enums/HomeSectionType.cs53
-rw-r--r--Jellyfin.Data/Enums/IndexingKind.cs20
-rw-r--r--Jellyfin.Data/Enums/ScrollDirection.cs18
-rw-r--r--Jellyfin.Data/Enums/SortOrder.cs18
-rw-r--r--Jellyfin.Data/Enums/ViewType.cs38
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj5
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj9
-rw-r--r--Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs22
-rw-r--r--Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs34
-rw-r--r--Jellyfin.Drawing.Skia/SkiaCodecException.cs2
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs525
-rw-r--r--Jellyfin.Drawing.Skia/SkiaException.cs2
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs118
-rw-r--r--Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs52
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs5
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj4
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs17
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbProvider.cs14
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs459
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs132
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs149
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs25
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs28
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs88
-rw-r--r--Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs12
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs197
-rw-r--r--Jellyfin.Server/CoreAppHost.cs18
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs35
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs206
-rw-r--r--Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs21
-rw-r--r--Jellyfin.Server/Formatters/CssOutputFormatter.cs36
-rw-r--r--Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs23
-rw-r--r--Jellyfin.Server/Formatters/XmlOutputFormatter.cs31
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj6
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs155
-rw-r--r--Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs78
-rw-r--r--Jellyfin.Server/Migrations/IMigrationRoutine.cs5
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs7
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs174
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs49
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs3
-rw-r--r--Jellyfin.Server/Models/ServerCorsPolicy.cs30
-rw-r--r--Jellyfin.Server/Program.cs38
-rw-r--r--Jellyfin.Server/Startup.cs18
-rw-r--r--MediaBrowser.Api/ApiEntryPoint.cs678
-rw-r--r--MediaBrowser.Api/Attachments/AttachmentService.cs63
-rw-r--r--MediaBrowser.Api/BaseApiService.cs416
-rw-r--r--MediaBrowser.Api/BrandingService.cs44
-rw-r--r--MediaBrowser.Api/ChannelService.cs340
-rw-r--r--MediaBrowser.Api/ConfigurationService.cs144
-rw-r--r--MediaBrowser.Api/Devices/DeviceService.cs103
-rw-r--r--MediaBrowser.Api/DisplayPreferencesService.cs101
-rw-r--r--MediaBrowser.Api/EnvironmentService.cs285
-rw-r--r--MediaBrowser.Api/FilterService.cs248
-rw-r--r--MediaBrowser.Api/IHasDtoOptions.cs13
-rw-r--r--MediaBrowser.Api/IHasItemFields.cs49
-rw-r--r--MediaBrowser.Api/Images/ImageByNameService.cs277
-rw-r--r--MediaBrowser.Api/Images/ImageRequest.cs100
-rw-r--r--MediaBrowser.Api/Images/ImageService.cs906
-rw-r--r--MediaBrowser.Api/Images/RemoteImageService.cs296
-rw-r--r--MediaBrowser.Api/ItemLookupService.cs336
-rw-r--r--MediaBrowser.Api/ItemRefreshService.cs83
-rw-r--r--MediaBrowser.Api/Library/LibraryService.cs1124
-rw-r--r--MediaBrowser.Api/Library/LibraryStructureService.cs412
-rw-r--r--MediaBrowser.Api/LiveTv/LiveTvService.cs1287
-rw-r--r--MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs80
-rw-r--r--MediaBrowser.Api/LocalizationService.cs111
-rw-r--r--MediaBrowser.Api/MediaBrowser.Api.csproj23
-rw-r--r--MediaBrowser.Api/Movies/CollectionService.cs104
-rw-r--r--MediaBrowser.Api/Movies/MoviesService.cs414
-rw-r--r--MediaBrowser.Api/Movies/TrailersService.cs88
-rw-r--r--MediaBrowser.Api/Music/AlbumsService.cs132
-rw-r--r--MediaBrowser.Api/Music/InstantMixService.cs196
-rw-r--r--MediaBrowser.Api/PackageService.cs197
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs1008
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs343
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs1230
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs164
-rw-r--r--MediaBrowser.Api/Playback/Hls/VideoHlsService.cs173
-rw-r--r--MediaBrowser.Api/Playback/Progressive/AudioService.cs95
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs442
-rw-r--r--MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs182
-rw-r--r--MediaBrowser.Api/Playback/Progressive/VideoService.cs131
-rw-r--r--MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs44
-rw-r--r--MediaBrowser.Api/Playback/StreamRequest.cs37
-rw-r--r--MediaBrowser.Api/Playback/StreamState.cs143
-rw-r--r--MediaBrowser.Api/Playback/UniversalAudioService.cs401
-rw-r--r--MediaBrowser.Api/PlaylistService.cs216
-rw-r--r--MediaBrowser.Api/PluginService.cs277
-rw-r--r--MediaBrowser.Api/Properties/AssemblyInfo.cs23
-rw-r--r--MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs234
-rw-r--r--MediaBrowser.Api/SearchService.cs332
-rw-r--r--MediaBrowser.Api/Sessions/ApiKeyService.cs85
-rw-r--r--MediaBrowser.Api/Sessions/SessionService.cs499
-rw-r--r--MediaBrowser.Api/Subtitles/SubtitleService.cs302
-rw-r--r--MediaBrowser.Api/SuggestionsService.cs103
-rw-r--r--MediaBrowser.Api/SyncPlay/SyncPlayService.cs279
-rw-r--r--MediaBrowser.Api/SyncPlay/TimeSyncService.cs52
-rw-r--r--MediaBrowser.Api/System/ActivityLogService.cs66
-rw-r--r--MediaBrowser.Api/System/SystemService.cs221
-rw-r--r--MediaBrowser.Api/TvShowsService.cs497
-rw-r--r--MediaBrowser.Api/UserLibrary/ArtistsService.cs143
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs388
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs478
-rw-r--r--MediaBrowser.Api/UserLibrary/GenresService.cs140
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs514
-rw-r--r--MediaBrowser.Api/UserLibrary/MusicGenresService.cs124
-rw-r--r--MediaBrowser.Api/UserLibrary/PersonsService.cs146
-rw-r--r--MediaBrowser.Api/UserLibrary/PlaystateService.cs456
-rw-r--r--MediaBrowser.Api/UserLibrary/StudiosService.cs132
-rw-r--r--MediaBrowser.Api/UserLibrary/UserLibraryService.cs575
-rw-r--r--MediaBrowser.Api/UserLibrary/UserViewsService.cs148
-rw-r--r--MediaBrowser.Api/UserLibrary/YearsService.cs131
-rw-r--r--MediaBrowser.Api/UserService.cs598
-rw-r--r--MediaBrowser.Api/VideosService.cs193
-rw-r--r--MediaBrowser.Common/Extensions/HttpContextExtensions.cs33
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs56
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs37
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs56
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs4
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs33
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj4
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs2
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs24
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs18
-rw-r--r--MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs5
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs19
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs3
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs15
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs26
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs49
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs3
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs7
-rw-r--r--MediaBrowser.Controller/Library/Profiler.cs17
-rw-r--r--MediaBrowser.Controller/Library/TVUtils.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs2
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs438
-rw-r--r--MediaBrowser.Controller/Net/AuthenticatedAttribute.cs4
-rw-r--r--MediaBrowser.Controller/Net/IAuthService.cs18
-rw-r--r--MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs53
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs1
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs5
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteImageProvider.cs3
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs3
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupInfo.cs22
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupMember.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs2
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs19
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs6
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs19
-rw-r--r--MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs34
-rw-r--r--MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs38
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs32
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs11
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs91
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj13
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaChapter.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs82
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/AssParser.cs25
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs5
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs11
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs50
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs92
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs9
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs2
-rw-r--r--MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs8
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs13
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs2
-rw-r--r--MediaBrowser.Model/Dlna/DlnaMaps.cs8
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs23
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs2
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs26
-rw-r--r--MediaBrowser.Model/Entities/DisplayPreferencesDto.cs (renamed from MediaBrowser.Model/Entities/DisplayPreferences.cs)12
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs219
-rw-r--r--MediaBrowser.Model/Entities/ScrollDirection.cs18
-rw-r--r--MediaBrowser.Model/Entities/SortOrder.cs18
-rw-r--r--MediaBrowser.Model/Extensions/ListHelper.cs29
-rw-r--r--MediaBrowser.Model/IO/IODefaults.cs7
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs2
-rw-r--r--MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs2
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOption.cs13
-rw-r--r--MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs3
-rw-r--r--MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs6
-rw-r--r--MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs6
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs37
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs87
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs77
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs337
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj11
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs15
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs21
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs1
-rw-r--r--MediaBrowser.Providers/Music/Extensions.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html42
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs215
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html62
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html35
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs132
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs74
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs67
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs32
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs56
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs138
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs194
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs67
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs40
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs125
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs16
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs45
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs2
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs2
-rw-r--r--MediaBrowser.WebDashboard/Api/DashboardService.cs340
-rw-r--r--MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj42
-rw-r--r--MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs21
-rw-r--r--MediaBrowser.WebDashboard/ServerEntryPoint.cs42
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs22
-rw-r--r--MediaBrowser.sln12
-rw-r--r--README.md7
-rw-r--r--RSSDP/RSSDP.csproj2
-rw-r--r--SharedVersion.cs4
-rw-r--r--build.yaml2
-rwxr-xr-xbump_version3
-rw-r--r--debian/changelog12
-rw-r--r--debian/conf/jellyfin4
-rw-r--r--debian/control5
-rw-r--r--debian/metapackage/jellyfin2
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rwxr-xr-xdeployment/build.windows.amd6410
-rw-r--r--fedora/jellyfin.spec4
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs2
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs53
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs46
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs62
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs58
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs31
-rw-r--r--tests/Jellyfin.Api.Tests/GetPathValueTests.cs45
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj11
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs81
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj2
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs14
-rw-r--r--tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs2
-rw-r--r--tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj5
-rw-r--r--windows/build-jellyfin.ps12
554 files changed, 31654 insertions, 26856 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 635aa759c..b558d2a6f 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -12,10 +12,12 @@ parameters:
jobs:
- job: CompatibilityCheck
displayName: Compatibility Check
+ dependsOn: Build
+ condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
pool:
vmImage: "${{ parameters.LinuxImage }}"
- # only execute for pull requests
- condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
strategy:
matrix:
${{ each Package in parameters.Packages }}:
@@ -23,7 +25,7 @@ jobs:
NugetPackageName: ${{ Package.value.NugetPackageName }}
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
maxParallel: 2
- dependsOn: Build
+
steps:
- checkout: none
@@ -34,32 +36,33 @@ jobs:
version: ${{ parameters.DotNetSdkVersion }}
- task: DotNetCoreCLI@2
- displayName: 'Install ABI CompatibilityChecker tool'
+ displayName: 'Install ABI CompatibilityChecker Tool'
inputs:
command: custom
custom: tool
arguments: 'update compatibilitychecker -g'
- task: DownloadPipelineArtifact@2
- displayName: "Download New Assembly Build Artifact"
+ displayName: 'Download New Assembly Build Artifact'
inputs:
- source: "current"
+ source: 'current'
artifact: "$(NugetPackageName)"
path: "$(System.ArtifactsDirectory)/new-artifacts"
runVersion: "latest"
- task: CopyFiles@2
- displayName: "Copy New Assembly Build Artifact"
+ displayName: 'Copy New Assembly Build Artifact'
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
- contents: "**/*.dll"
+ contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true
overWrite: true
flattenFolders: true
- task: DownloadPipelineArtifact@2
- displayName: "Download Reference Assembly Build Artifact"
+ displayName: 'Download Reference Assembly Build Artifact'
+ enabled: false
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
@@ -70,18 +73,19 @@ jobs:
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
- task: CopyFiles@2
- displayName: "Copy Reference Assembly Build Artifact"
+ displayName: 'Copy Reference Assembly Build Artifact'
+ enabled: false
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
- contents: "**/*.dll"
+ contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true
overWrite: true
flattenFolders: true
- # The `--warnings-only` switch will swallow the return code and not emit any errors.
- task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool'
+ enabled: false
inputs:
command: custom
custom: compat
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index b34253190..003d5baf0 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -35,7 +35,6 @@ jobs:
steps:
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
- condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
displayName: 'Run Dockerfile (unstable)'
@@ -47,14 +46,19 @@ jobs:
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
- condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
artifactName: 'jellyfin-server-$(BuildConfiguration)'
+ - task: SSH@0
+ displayName: 'Create target directory on repository server'
+ inputs:
+ sshEndpoint: repository
+ runOptions: 'inline'
+ inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
- condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
@@ -76,7 +80,15 @@ jobs:
pool:
vmImage: 'ubuntu-latest'
+ variables:
+ - name: JellyfinVersion
+ value: 0.0.0
+
steps:
+ - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+ displayName: Set release version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -101,9 +113,10 @@ jobs:
containerRegistry: Docker Hub
tags: |
stable-$(Build.BuildNumber)-$(BuildConfiguration)
- stable-$(BuildConfiguration)
+ $(JellyfinVersion)-$(BuildConfiguration)
- job: CollectArtifacts
+ timeoutInMinutes: 10
displayName: 'Collect Artifacts'
dependsOn:
- BuildPackage
@@ -119,13 +132,35 @@ jobs:
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
sshEndpoint: repository
- runOptions: 'inline'
- inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable'
+ runOptions: 'commands'
+ commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
- task: SSH@0
displayName: 'Update Stable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs:
sshEndpoint: repository
- runOptions: 'inline'
- inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)'
+ runOptions: 'commands'
+ commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
+
+- job: PublishNuget
+ displayName: 'Publish NuGet packages'
+ dependsOn:
+ - BuildPackage
+ condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
+
+ pool:
+ vmImage: 'ubuntu-latest'
+
+ steps:
+ - task: NuGetCommand@2
+ inputs:
+ command: 'pack'
+ packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
+ packDestination: '$(Build.ArtifactStagingDirectory)'
+
+ - task: NuGetCommand@2
+ inputs:
+ command: 'push'
+ packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
+ includeNugetOrg: 'true'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index c9013b3b8..0c86c0171 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -15,11 +15,13 @@ trigger:
batch: true
jobs:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-main.yml
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: $(RestoreBuildProjects)
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-test.yml
parameters:
ImageNames:
@@ -27,6 +29,7 @@ jobs:
Windows: 'windows-latest'
macOS: 'macos-latest'
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml
parameters:
Packages:
@@ -44,4 +47,5 @@ jobs:
AssemblyFileName: MediaBrowser.Common.dll
LinuxImage: 'ubuntu-latest'
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 0f698bfa4..bf1bd65cb 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,11 +6,21 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- // If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
- // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
+ "console": "internalConsole",
+ "stopAtEntry": false,
+ "internalConsoleOptions": "openOnSessionStart"
+ },
+ {
+ "name": ".NET Core Launch (nowebclient)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+ "args": ["--nowebclient"],
+ "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs
deleted file mode 100644
index 7fba2184a..000000000
--- a/Emby.Dlna/Api/DlnaServerService.cs
+++ /dev/null
@@ -1,383 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-using Emby.Dlna.Main;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Dlna.Api
-{
- [Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")]
- [Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")]
- public class GetDescriptionXml
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UuId { get; set; }
- }
-
- [Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
- [Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
- public class GetContentDirectory
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UuId { get; set; }
- }
-
- [Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
- [Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
- public class GetConnnectionManager
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UuId { get; set; }
- }
-
- [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
- [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
- public class GetMediaReceiverRegistrar
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UuId { get; set; }
- }
-
- [Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")]
- public class ProcessContentDirectoryControlRequest : IRequiresRequestStream
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UuId { get; set; }
-
- public Stream RequestStream { get; set; }
- }
-
- [Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")]
- public class ProcessConnectionManagerControlRequest : IRequiresRequestStream
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UuId { get; set; }
-
- public Stream RequestStream { get; set; }
- }
-
- [Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")]
- public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UuId { get; set; }
-
- public Stream RequestStream { get; set; }
- }
-
- [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
- [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
- public class ProcessMediaReceiverRegistrarEventRequest
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
- public string UuId { get; set; }
- }
-
- [Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
- [Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
- public class ProcessContentDirectoryEventRequest
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
- public string UuId { get; set; }
- }
-
- [Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
- [Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
- public class ProcessConnectionManagerEventRequest
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
- public string UuId { get; set; }
- }
-
- [Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")]
- [Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")]
- public class GetIcon
- {
- [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string UuId { get; set; }
-
- [ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Filename { get; set; }
- }
-
- public class DlnaServerService : IService, IRequiresRequest
- {
- private const string XMLContentType = "text/xml; charset=UTF-8";
-
- private readonly IDlnaManager _dlnaManager;
- private readonly IHttpResultFactory _resultFactory;
- private readonly IServerConfigurationManager _configurationManager;
-
- public IRequest Request { get; set; }
-
- private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
-
- private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager;
-
- private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
-
- public DlnaServerService(
- IDlnaManager dlnaManager,
- IHttpResultFactory httpResultFactory,
- IServerConfigurationManager configurationManager)
- {
- _dlnaManager = dlnaManager;
- _resultFactory = httpResultFactory;
- _configurationManager = configurationManager;
- }
-
- private string GetHeader(string name)
- {
- return Request.Headers[name];
- }
-
- public object Get(GetDescriptionXml request)
- {
- var url = Request.AbsoluteUri;
- var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
- var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress);
-
- var cacheLength = TimeSpan.FromDays(1);
- var cacheKey = Request.RawUrl.GetMD5();
- var bytes = Encoding.UTF8.GetBytes(xml);
-
- return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes)));
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetContentDirectory request)
- {
- var xml = ContentDirectory.GetServiceXml();
-
- return _resultFactory.GetResult(Request, xml, XMLContentType);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetMediaReceiverRegistrar request)
- {
- var xml = MediaReceiverRegistrar.GetServiceXml();
-
- return _resultFactory.GetResult(Request, xml, XMLContentType);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetConnnectionManager request)
- {
- var xml = ConnectionManager.GetServiceXml();
-
- return _resultFactory.GetResult(Request, xml, XMLContentType);
- }
-
- public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
- {
- var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
-
- return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
- }
-
- public async Task<object> Post(ProcessContentDirectoryControlRequest request)
- {
- var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
-
- return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
- }
-
- public async Task<object> Post(ProcessConnectionManagerControlRequest request)
- {
- var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
-
- return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
- }
-
- private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
- {
- var id = GetPathValue(2).ToString();
-
- return service.ProcessControlRequestAsync(new ControlRequest
- {
- Headers = Request.Headers,
- InputXml = requestStream,
- TargetServerUuId = id,
- RequestedUrl = Request.AbsoluteUri
- });
- }
-
- // Copied from MediaBrowser.Api/BaseApiService.cs
- // TODO: Remove code duplication
- /// <summary>
- /// Gets the path segment at the specified index.
- /// </summary>
- /// <param name="index">The index of the path segment.</param>
- /// <returns>The path segment at the specified index.</returns>
- /// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
- /// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
- protected internal ReadOnlySpan<char> GetPathValue(int index)
- {
- static void ThrowIndexOutOfRangeException()
- => throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
-
- static void ThrowInvalidDataException()
- => throw new InvalidDataException("Path doesn't start with the base url.");
-
- ReadOnlySpan<char> path = Request.PathInfo;
-
- // Remove the protocol part from the url
- int pos = path.LastIndexOf("://");
- if (pos != -1)
- {
- path = path.Slice(pos + 3);
- }
-
- // Remove the query string
- pos = path.LastIndexOf('?');
- if (pos != -1)
- {
- path = path.Slice(0, pos);
- }
-
- // Remove the domain
- pos = path.IndexOf('/');
- if (pos != -1)
- {
- path = path.Slice(pos);
- }
-
- // Remove base url
- string baseUrl = _configurationManager.Configuration.BaseUrl;
- int baseUrlLen = baseUrl.Length;
- if (baseUrlLen != 0)
- {
- if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
- {
- path = path.Slice(baseUrlLen);
- }
- else
- {
- // The path doesn't start with the base url,
- // how did we get here?
- ThrowInvalidDataException();
- }
- }
-
- // Remove leading /
- path = path.Slice(1);
-
- // Backwards compatibility
- const string Emby = "emby/";
- if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
- {
- path = path.Slice(Emby.Length);
- }
-
- const string MediaBrowser = "mediabrowser/";
- if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
- {
- path = path.Slice(MediaBrowser.Length);
- }
-
- // Skip segments until we are at the right index
- for (int i = 0; i < index; i++)
- {
- pos = path.IndexOf('/');
- if (pos == -1)
- {
- ThrowIndexOutOfRangeException();
- }
-
- path = path.Slice(pos + 1);
- }
-
- // Remove the rest
- pos = path.IndexOf('/');
- if (pos != -1)
- {
- path = path.Slice(0, pos);
- }
-
- return path;
- }
-
- public object Get(GetIcon request)
- {
- var contentType = "image/" + Path.GetExtension(request.Filename)
- .TrimStart('.')
- .ToLowerInvariant();
-
- var cacheLength = TimeSpan.FromDays(365);
- var cacheKey = Request.RawUrl.GetMD5();
-
- return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream));
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Subscribe(ProcessContentDirectoryEventRequest request)
- {
- return ProcessEventRequest(ContentDirectory);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Subscribe(ProcessConnectionManagerEventRequest request)
- {
- return ProcessEventRequest(ConnectionManager);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
- {
- return ProcessEventRequest(MediaReceiverRegistrar);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Unsubscribe(ProcessContentDirectoryEventRequest request)
- {
- return ProcessEventRequest(ContentDirectory);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Unsubscribe(ProcessConnectionManagerEventRequest request)
- {
- return ProcessEventRequest(ConnectionManager);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request)
- {
- return ProcessEventRequest(MediaReceiverRegistrar);
- }
-
- private object ProcessEventRequest(IEventManager eventManager)
- {
- var subscriptionId = GetHeader("SID");
-
- if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase))
- {
- var notificationType = GetHeader("NT");
-
- var callback = GetHeader("CALLBACK");
- var timeoutString = GetHeader("TIMEOUT");
-
- if (string.IsNullOrEmpty(notificationType))
- {
- return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback));
- }
-
- return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback));
- }
-
- return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId));
- }
-
- private object GetSubscriptionResponse(EventSubscriptionResponse response)
- {
- return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers);
- }
- }
-}
diff --git a/Emby.Dlna/Api/DlnaService.cs b/Emby.Dlna/Api/DlnaService.cs
deleted file mode 100644
index 5f984bb33..000000000
--- a/Emby.Dlna/Api/DlnaService.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Dlna.Api
-{
- [Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
- public class GetProfileInfos : IReturn<DeviceProfileInfo[]>
- {
- }
-
- [Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")]
- public class DeleteProfile : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")]
- public class GetDefaultProfile : IReturn<DeviceProfile>
- {
- }
-
- [Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")]
- public class GetProfile : IReturn<DeviceProfile>
- {
- [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")]
- public class UpdateProfile : DeviceProfile, IReturnVoid
- {
- }
-
- [Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")]
- public class CreateProfile : DeviceProfile, IReturnVoid
- {
- }
-
- [Authenticated(Roles = "Admin")]
- public class DlnaService : IService
- {
- private readonly IDlnaManager _dlnaManager;
-
- public DlnaService(IDlnaManager dlnaManager)
- {
- _dlnaManager = dlnaManager;
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetProfileInfos request)
- {
- return _dlnaManager.GetProfileInfos().ToArray();
- }
-
- public object Get(GetProfile request)
- {
- return _dlnaManager.GetProfile(request.Id);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetDefaultProfile request)
- {
- return _dlnaManager.GetDefaultProfile();
- }
-
- public void Delete(DeleteProfile request)
- {
- _dlnaManager.DeleteProfile(request.Id);
- }
-
- public void Post(UpdateProfile request)
- {
- _dlnaManager.UpdateProfile(request);
- }
-
- public void Post(CreateProfile request)
- {
- _dlnaManager.CreateProfile(request);
- }
- }
-}
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 291de5245..00821bf78 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -11,6 +11,7 @@ using System.Xml;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs
index 140ef9b46..243b09786 100644
--- a/Emby.Dlna/ControlResponse.cs
+++ b/Emby.Dlna/ControlResponse.cs
@@ -16,5 +16,11 @@ namespace Emby.Dlna
public string Xml { get; set; }
public bool IsSuccessful { get; set; }
+
+ /// <inheritdoc />
+ public override string ToString()
+ {
+ return Xml;
+ }
}
}
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 66baa9512..70e358019 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -364,7 +364,8 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
}
- var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
+ var mediaProfile = _profile.GetVideoMediaProfile(
+ streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 5911a73ef..ce4be7b51 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -54,11 +54,15 @@ namespace Emby.Dlna
_appHost = appHost;
}
+ private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
+
+ private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
+
public async Task InitProfilesAsync()
{
try
{
- await ExtractSystemProfilesAsync();
+ await ExtractSystemProfilesAsync().ConfigureAwait(false);
LoadProfiles();
}
catch (Exception ex)
@@ -122,15 +126,15 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
- builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
- builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
- builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
- builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
- builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
- builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
- builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
- builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
- builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
+ builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
_logger.LogInformation(builder.ToString());
}
@@ -240,7 +244,7 @@ namespace Emby.Dlna
}
else
{
- var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
+ var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
_logger.LogDebug("No matching device profile found. {0}", headerString);
}
@@ -280,10 +284,6 @@ namespace Emby.Dlna
return false;
}
- private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
-
- private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
-
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
{
try
@@ -387,7 +387,7 @@ namespace Emby.Dlna
foreach (var name in _assembly.GetManifestResourceNames())
{
- if (!name.StartsWith(namespaceName))
+ if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
{
continue;
}
@@ -406,7 +406,7 @@ namespace Emby.Dlna
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
- await stream.CopyToAsync(fileStream);
+ await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}
@@ -509,7 +509,7 @@ namespace Emby.Dlna
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
}
- class InternalProfileInfo
+ private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/EventManager.cs
index 56c90c8b3..7d02f5e96 100644
--- a/Emby.Dlna/Eventing/EventManager.cs
+++ b/Emby.Dlna/Eventing/EventManager.cs
@@ -152,11 +152,15 @@ namespace Emby.Dlna.Eventing
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
foreach (var key in stateVariables.Keys)
{
- builder.Append("<e:property>");
- builder.Append("<" + key + ">");
- builder.Append(stateVariables[key]);
- builder.Append("</" + key + ">");
- builder.Append("</e:property>");
+ builder.Append("<e:property>")
+ .Append('<')
+ .Append(key)
+ .Append('>')
+ .Append(stateVariables[key])
+ .Append("</")
+ .Append(key)
+ .Append('>')
+ .Append("</e:property>");
}
builder.Append("</e:propertyset>");
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 9b9b57e97..a21d4cc11 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -54,11 +54,11 @@ namespace Emby.Dlna.Main
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer;
- internal IContentDirectory ContentDirectory { get; private set; }
+ public IContentDirectory ContentDirectory { get; private set; }
- internal IConnectionManager ConnectionManager { get; private set; }
+ public IConnectionManager ConnectionManager { get; private set; }
- internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
+ public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public static DlnaEntryPoint Current;
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index c5080b90f..86dd52a1b 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Security;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Emby.Dlna.Common;
-using Emby.Dlna.Server;
using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -19,6 +19,8 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
private Timer _timer;
public DeviceInfo Properties { get; set; }
@@ -55,16 +57,13 @@ namespace Emby.Dlna.PlayTo
private readonly ILogger _logger;
- private readonly IServerConfigurationManager _config;
-
public Action OnDeviceUnavailable { get; set; }
- public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
+ public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
{
Properties = deviceProperties;
_httpClient = httpClient;
_logger = logger;
- _config = config;
}
public void Start()
@@ -275,7 +274,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
- await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+ await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
.ConfigureAwait(false);
RestartTimer(true);
@@ -285,7 +284,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- url = url.Replace("&", "&amp;");
+ url = url.Replace("&", "&amp;", StringComparison.Ordinal);
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
@@ -297,8 +296,8 @@ namespace Emby.Dlna.PlayTo
var dictionary = new Dictionary<string, string>
{
- {"CurrentURI", url},
- {"CurrentURIMetaData", CreateDidlMeta(metaData)}
+ { "CurrentURI", url },
+ { "CurrentURIMetaData", CreateDidlMeta(metaData) }
};
var service = GetAvTransportService();
@@ -334,7 +333,7 @@ namespace Emby.Dlna.PlayTo
return string.Empty;
}
- return DescriptionXmlBuilder.Escape(value);
+ return SecurityElement.Escape(value);
}
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
@@ -732,10 +731,10 @@ namespace Emby.Dlna.PlayTo
}
var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
- var trackUri = trackUriElem == null ? null : trackUriElem.Value;
+ var trackUri = trackUriElem?.Value;
var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
- var duration = durationElem == null ? null : durationElem.Value;
+ var duration = durationElem?.Value;
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@@ -748,7 +747,7 @@ namespace Emby.Dlna.PlayTo
}
var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
- var position = positionElem == null ? null : positionElem.Value;
+ var position = positionElem?.Value;
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
@@ -819,7 +818,7 @@ namespace Emby.Dlna.PlayTo
// some devices send back invalid xml
try
{
- return XElement.Parse(xml.Replace("&", "&amp;"));
+ return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
}
catch (XmlException)
{
@@ -848,7 +847,7 @@ namespace Emby.Dlna.PlayTo
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
Title = container.GetValue(uPnpNamespaces.title),
IconUrl = container.GetValue(uPnpNamespaces.Artwork),
- SecondText = "",
+ SecondText = string.Empty,
Url = url,
ProtocolInfo = GetProtocolInfo(container),
MetaData = container.ToString()
@@ -941,12 +940,12 @@ namespace Emby.Dlna.PlayTo
return url;
}
- if (!url.Contains("/"))
+ if (!url.Contains('/', StringComparison.Ordinal))
{
url = "/dmr/" + url;
}
- if (!url.StartsWith("/"))
+ if (!url.StartsWith("/", StringComparison.Ordinal))
{
url = "/" + url;
}
@@ -981,7 +980,7 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo()
{
Name = string.Join(" ", friendlyNames),
- BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
+ BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
};
var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
@@ -1068,10 +1067,9 @@ namespace Emby.Dlna.PlayTo
}
}
- return new Device(deviceProperties, httpClient, logger, config);
+ return new Device(deviceProperties, httpClient, logger);
}
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private static DeviceIcon CreateIcon(XElement element)
{
if (element == null)
@@ -1222,7 +1220,7 @@ namespace Emby.Dlna.PlayTo
public override string ToString()
{
- return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
+ return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
}
}
}
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index c0ce3ab6e..dc797a691 100644
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ b/Emby.Dlna/PlayTo/TransportCommands.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Emby.Dlna.Common;
@@ -11,14 +12,16 @@ namespace Emby.Dlna.PlayTo
{
public class TransportCommands
{
+ private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
private List<StateVariable> _stateVariables = new List<StateVariable>();
+ private List<ServiceAction> _serviceActions = new List<ServiceAction>();
+
public List<StateVariable> StateVariables
{
get => _stateVariables;
set => _stateVariables = value;
}
- private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<ServiceAction> ServiceActions
{
get => _serviceActions;
@@ -123,7 +126,7 @@ namespace Emby.Dlna.PlayTo
}
}
- return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
+ return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
@@ -147,7 +150,7 @@ namespace Emby.Dlna.PlayTo
}
}
- return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
+ return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
@@ -170,7 +173,7 @@ namespace Emby.Dlna.PlayTo
}
}
- return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
+ return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
@@ -183,12 +186,10 @@ namespace Emby.Dlna.PlayTo
state.AllowedValues.FirstOrDefault() ??
value;
- return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
+ return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
}
- return string.Format("<{0}>{1}</{0}>", argument.Name, value);
+ return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
}
-
- private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
}
}
diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs
index 2347ebd0d..90a23a4a2 100644
--- a/Emby.Dlna/Profiles/DefaultProfile.cs
+++ b/Emby.Dlna/Profiles/DefaultProfile.cs
@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
public void AddXmlRootAttribute(string name, string value)
{
- var atts = XmlRootAttributes ?? new XmlAttribute[] { };
+ var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
var list = atts.ToList();
list.Add(new XmlAttribute
diff --git a/Emby.Dlna/Profiles/DenonAvrProfile.cs b/Emby.Dlna/Profiles/DenonAvrProfile.cs
index 73a87c499..a5ba0f36c 100644
--- a/Emby.Dlna/Profiles/DenonAvrProfile.cs
+++ b/Emby.Dlna/Profiles/DenonAvrProfile.cs
@@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles
},
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = System.Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/DirectTvProfile.cs b/Emby.Dlna/Profiles/DirectTvProfile.cs
index 5ca388167..f6f98b07d 100644
--- a/Emby.Dlna/Profiles/DirectTvProfile.cs
+++ b/Emby.Dlna/Profiles/DirectTvProfile.cs
@@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles
}
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = System.Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/Foobar2000Profile.cs b/Emby.Dlna/Profiles/Foobar2000Profile.cs
index ea3de686a..68e959770 100644
--- a/Emby.Dlna/Profiles/Foobar2000Profile.cs
+++ b/Emby.Dlna/Profiles/Foobar2000Profile.cs
@@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles
}
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = System.Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/MarantzProfile.cs b/Emby.Dlna/Profiles/MarantzProfile.cs
index 6cfcc3b82..24078ab29 100644
--- a/Emby.Dlna/Profiles/MarantzProfile.cs
+++ b/Emby.Dlna/Profiles/MarantzProfile.cs
@@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles
},
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = System.Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs
index 7161af738..28d639b6d 100644
--- a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs
+++ b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles
@@ -37,7 +38,7 @@ namespace Emby.Dlna.Profiles
}
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
index 42b066d52..238fe9f6b 100644
--- a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
+++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles
@@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
}
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
index fbdf2c18e..812a48151 100644
--- a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
+++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles
@@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
}
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
index ce32179a1..6bfff322e 100644
--- a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
+++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles
@@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
}
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
index aa1721d39..ec2529574 100644
--- a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
+++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles
@@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
}
};
- ResponseProfiles = new ResponseProfile[] { };
+ ResponseProfiles = Array.Empty<ResponseProfile>();
}
}
}
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 7143c3109..bca9e81cd 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Security;
using System.Text;
using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna;
@@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
foreach (var att in attributes)
{
- builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value);
+ builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
}
- builder.Append(">");
+ builder.Append('>');
builder.Append("<specVersion>");
builder.Append("<major>1</major>");
@@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
if (!EnableAbsoluteUrls)
{
- builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
+ builder.Append("<URLBase>")
+ .Append(SecurityElement.Escape(_serverAddress))
+ .Append("</URLBase>");
}
AppendDeviceInfo(builder);
@@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
AppendIconList(builder);
- builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>");
+ builder.Append("<presentationURL>")
+ .Append(SecurityElement.Escape(_serverAddress))
+ .Append("/web/index.html</presentationURL>");
AppendServiceList(builder);
builder.Append("</device>");
}
- private static readonly char[] s_escapeChars = new char[]
- {
- '<',
- '>',
- '"',
- '\'',
- '&'
- };
-
- private static readonly string[] s_escapeStringPairs = new[]
- {
- "<",
- "&lt;",
- ">",
- "&gt;",
- "\"",
- "&quot;",
- "'",
- "&apos;",
- "&",
- "&amp;"
- };
-
- private static string GetEscapeSequence(char c)
- {
- int num = s_escapeStringPairs.Length;
- for (int i = 0; i < num; i += 2)
- {
- string text = s_escapeStringPairs[i];
- string result = s_escapeStringPairs[i + 1];
- if (text[0] == c)
- {
- return result;
- }
- }
-
- return c.ToString(CultureInfo.InvariantCulture);
- }
-
- /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
- /// <returns>The input string with invalid characters replaced.</returns>
- /// <param name="str">The string within which to escape invalid characters. </param>
- public static string Escape(string str)
- {
- if (str == null)
- {
- return null;
- }
-
- StringBuilder stringBuilder = null;
- int length = str.Length;
- int num = 0;
- while (true)
- {
- int num2 = str.IndexOfAny(s_escapeChars, num);
- if (num2 == -1)
- {
- break;
- }
-
- if (stringBuilder == null)
- {
- stringBuilder = new StringBuilder();
- }
-
- stringBuilder.Append(str, num, num2 - num);
- stringBuilder.Append(GetEscapeSequence(str[num2]));
- num = num2 + 1;
- }
-
- if (stringBuilder == null)
- {
- return str;
- }
-
- stringBuilder.Append(str, num, length - num);
- return stringBuilder.ToString();
- }
-
private void AppendDeviceProperties(StringBuilder builder)
{
builder.Append("<dlna:X_DLNACAP/>");
@@ -187,32 +113,54 @@ namespace Emby.Dlna.Server
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
- builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
- builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
- builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
-
- builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
- builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
-
- builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
- builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
+ builder.Append("<friendlyName>")
+ .Append(SecurityElement.Escape(GetFriendlyName()))
+ .Append("</friendlyName>");
+ builder.Append("<manufacturer>")
+ .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
+ .Append("</manufacturer>");
+ builder.Append("<manufacturerURL>")
+ .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
+ .Append("</manufacturerURL>");
+
+ builder.Append("<modelDescription>")
+ .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
+ .Append("</modelDescription>");
+ builder.Append("<modelName>")
+ .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
+ .Append("</modelName>");
+
+ builder.Append("<modelNumber>")
+ .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
+ .Append("</modelNumber>");
+ builder.Append("<modelURL>")
+ .Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
+ .Append("</modelURL>");
if (string.IsNullOrEmpty(_profile.SerialNumber))
{
- builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
+ builder.Append("<serialNumber>")
+ .Append(SecurityElement.Escape(_serverId))
+ .Append("</serialNumber>");
}
else
{
- builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
+ builder.Append("<serialNumber>")
+ .Append(SecurityElement.Escape(_profile.SerialNumber))
+ .Append("</serialNumber>");
}
builder.Append("<UPC/>");
- builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>");
+ builder.Append("<UDN>uuid:")
+ .Append(SecurityElement.Escape(_serverUdn))
+ .Append("</UDN>");
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
{
- builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>");
+ builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
+ .Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
+ .Append("</av:aggregationFlags>");
}
}
@@ -250,11 +198,21 @@ namespace Emby.Dlna.Server
{
builder.Append("<icon>");
- builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
- builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
- builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
- builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
- builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
+ builder.Append("<mimetype>")
+ .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
+ .Append("</mimetype>");
+ builder.Append("<width>")
+ .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+ .Append("</width>");
+ builder.Append("<height>")
+ .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+ .Append("</height>");
+ builder.Append("<depth>")
+ .Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
+ .Append("</depth>");
+ builder.Append("<url>")
+ .Append(BuildUrl(icon.Url))
+ .Append("</url>");
builder.Append("</icon>");
}
@@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
{
builder.Append("<service>");
- builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
- builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
- builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
- builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
- builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
+ builder.Append("<serviceType>")
+ .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
+ .Append("</serviceType>");
+ builder.Append("<serviceId>")
+ .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
+ .Append("</serviceId>");
+ builder.Append("<SCPDURL>")
+ .Append(BuildUrl(service.ScpdUrl))
+ .Append("</SCPDURL>");
+ builder.Append("<controlURL>")
+ .Append(BuildUrl(service.ControlUrl))
+ .Append("</controlURL>");
+ builder.Append("<eventSubURL>")
+ .Append(BuildUrl(service.EventSubUrl))
+ .Append("</eventSubURL>");
builder.Append("</service>");
}
@@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + url;
}
- return Escape(url);
+ return SecurityElement.Escape(url);
}
private IEnumerable<DeviceIcon> GetIcons()
diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs
index af557aa14..6c7d6f846 100644
--- a/Emby.Dlna/Service/ServiceXmlBuilder.cs
+++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs
@@ -1,9 +1,9 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Security;
using System.Text;
using Emby.Dlna.Common;
-using Emby.Dlna.Server;
namespace Emby.Dlna.Service
{
@@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
{
builder.Append("<action>");
- builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
+ builder.Append("<name>")
+ .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+ .Append("</name>");
builder.Append("<argumentList>");
@@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
{
builder.Append("<argument>");
- builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>");
- builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>");
- builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
+ builder.Append("<name>")
+ .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
+ .Append("</name>");
+ builder.Append("<direction>")
+ .Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
+ .Append("</direction>");
+ builder.Append("<relatedStateVariable>")
+ .Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
+ .Append("</relatedStateVariable>");
builder.Append("</argument>");
}
@@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
{
var sendEvents = item.SendsEvents ? "yes" : "no";
- builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">");
+ builder.Append("<stateVariable sendEvents=\"")
+ .Append(sendEvents)
+ .Append("\">");
- builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
- builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>");
+ builder.Append("<name>")
+ .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+ .Append("</name>");
+ builder.Append("<dataType>")
+ .Append(SecurityElement.Escape(item.DataType ?? string.Empty))
+ .Append("</dataType>");
if (item.AllowedValues.Length > 0)
{
builder.Append("<allowedValueList>");
foreach (var allowedValue in item.AllowedValues)
{
- builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>");
+ builder.Append("<allowedValue>")
+ .Append(SecurityElement.Escape(allowedValue))
+ .Append("</allowedValue>");
}
builder.Append("</allowedValueList>");
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 8696cb280..f585b90ca 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -448,21 +448,21 @@ namespace Emby.Drawing
/// or
/// filename.
/// </exception>
- public string GetCachePath(string path, string filename)
+ public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
{
- if (string.IsNullOrEmpty(path))
+ if (path.IsEmpty)
{
- throw new ArgumentNullException(nameof(path));
+ throw new ArgumentException("Path can't be empty.", nameof(path));
}
- if (string.IsNullOrEmpty(filename))
+ if (path.IsEmpty)
{
- throw new ArgumentNullException(nameof(filename));
+ throw new ArgumentException("Filename can't be empty.", nameof(filename));
}
- var prefix = filename.Substring(0, 1);
+ var prefix = filename.Slice(0, 1);
- return Path.Combine(path, prefix, filename);
+ return Path.Join(path, prefix, filename);
}
/// <inheritdoc />
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 1b343790e..d1e17f416 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
CleanDateTimes = new[]
{
- @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*",
- @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
+ @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
+ @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
};
CleanStrings = new[]
@@ -277,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
- new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$")
+ new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
{
IsNamed = true
},
@@ -300,32 +300,32 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming
// [bar] Foo - 1 [baz]
- new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
+ new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
- new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
+ new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{
IsNamed = true
},
- new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$")
+ new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{
IsNamed = true
},
- new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$")
+ new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
{
IsNamed = true
},
- new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$")
+ new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{
IsNamed = true
},
// "01.avi"
- new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
+ new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
{
IsOptimistic = true,
IsNamed = true
@@ -335,34 +335,34 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi"
- new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
+ new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
{
IsOptimistic = true,
IsNamed = true
},
// "01.blah.avi"
- new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$")
+ new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
{
IsOptimistic = true,
IsNamed = true
},
// "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
- new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+ new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{
IsOptimistic = true,
IsNamed = true
},
// "01 episode title.avi"
- new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$")
+ new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
{
IsOptimistic = true,
IsNamed = true
},
// "Episode 16", "Episode 16 - Title"
- new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+ new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{
IsOptimistic = true,
IsNamed = true
@@ -625,17 +625,17 @@ namespace Emby.Naming.Common
AudioBookPartsExpressions = new[]
{
// Detect specified chapters, like CH 01
- @"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
+ @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02
- @"p(?:ar)?t[\s_-]?(?<part>\d+)",
+ @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
// Chapter is often beginning of filename
- @"^(?<chapter>\d+)",
+ "^(?<chapter>[0-9]+)",
// Part if often ending of filename
- @"(?<part>\d+)$",
+ "(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
- @"(?<chapter>\d+)_(?<part>\d+)",
+ "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
- @"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
+ @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
};
var extensions = VideoFileExtensions.ToList();
@@ -675,16 +675,16 @@ namespace Emby.Naming.Common
MultipleEpisodeExpressions = new string[]
{
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$"
+ @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
}.Select(i => new EpisodeExpression(i)
{
IsNamed = true
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index 2fa6b4353..d2e324dda 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -77,7 +77,7 @@ namespace Emby.Naming.TV
if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{
- var testFilename = filename.Substring(1);
+ var testFilename = filename.AsSpan().Slice(1);
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs
deleted file mode 100644
index 1ff8a5026..000000000
--- a/Emby.Notifications/Api/NotificationsService.cs
+++ /dev/null
@@ -1,191 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Notifications.Api
-{
- [Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")]
- public class GetNotifications : IReturn<NotificationResult>
- {
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UserId { get; set; } = string.Empty;
-
- [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsRead { get; set; }
-
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
- }
-
- public class Notification
- {
- public string Id { get; set; } = string.Empty;
-
- public string UserId { get; set; } = string.Empty;
-
- public DateTime Date { get; set; }
-
- public bool IsRead { get; set; }
-
- public string Name { get; set; } = string.Empty;
-
- public string Description { get; set; } = string.Empty;
-
- public string Url { get; set; } = string.Empty;
-
- public NotificationLevel Level { get; set; }
- }
-
- public class NotificationResult
- {
- public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
-
- public int TotalRecordCount { get; set; }
- }
-
- public class NotificationsSummary
- {
- public int UnreadCount { get; set; }
-
- public NotificationLevel MaxUnreadNotificationLevel { get; set; }
- }
-
- [Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")]
- public class GetNotificationsSummary : IReturn<NotificationsSummary>
- {
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string UserId { get; set; } = string.Empty;
- }
-
- [Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
- public class GetNotificationTypes : IReturn<List<NotificationTypeInfo>>
- {
- }
-
- [Route("/Notifications/Services", "GET", Summary = "Gets notification types")]
- public class GetNotificationServices : IReturn<List<NameIdPair>>
- {
- }
-
- [Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")]
- public class AddAdminNotification : IReturnVoid
- {
- [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Name { get; set; } = string.Empty;
-
- [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Description { get; set; } = string.Empty;
-
- [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string? ImageUrl { get; set; }
-
- [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string? Url { get; set; }
-
- [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public NotificationLevel Level { get; set; }
- }
-
- [Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")]
- public class MarkRead : IReturnVoid
- {
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string UserId { get; set; } = string.Empty;
-
- [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
- public string Ids { get; set; } = string.Empty;
- }
-
- [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
- public class MarkUnread : IReturnVoid
- {
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string UserId { get; set; } = string.Empty;
-
- [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
- public string Ids { get; set; } = string.Empty;
- }
-
- [Authenticated]
- public class NotificationsService : IService
- {
- private readonly INotificationManager _notificationManager;
- private readonly IUserManager _userManager;
-
- public NotificationsService(INotificationManager notificationManager, IUserManager userManager)
- {
- _notificationManager = notificationManager;
- _userManager = userManager;
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetNotificationTypes request)
- {
- return _notificationManager.GetNotificationTypes();
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetNotificationServices request)
- {
- return _notificationManager.GetNotificationServices().ToList();
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetNotificationsSummary request)
- {
- return new NotificationsSummary();
- }
-
- public Task Post(AddAdminNotification request)
- {
- // This endpoint really just exists as post of a real with sickbeard
- var notification = new NotificationRequest
- {
- Date = DateTime.UtcNow,
- Description = request.Description,
- Level = request.Level,
- Name = request.Name,
- Url = request.Url,
- UserIds = _userManager.Users
- .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
- .Select(user => user.Id)
- .ToArray()
- };
-
- return _notificationManager.SendNotification(notification, CancellationToken.None);
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public void Post(MarkRead request)
- {
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public void Post(MarkUnread request)
- {
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetNotifications request)
- {
- return new NotificationResult();
- }
- }
-}
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 0b681fddf..4c9ab33a7 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
using System.IO;
using System.Linq;
@@ -22,7 +24,7 @@ namespace Emby.Server.Implementations.AppBase
{
object configuration;
- byte[] buffer = null;
+ byte[]? buffer = null;
// Use try/catch to avoid the extra file system lookup using File.Exists
try
@@ -36,19 +38,23 @@ namespace Emby.Server.Implementations.AppBase
configuration = Activator.CreateInstance(type);
}
- using var stream = new MemoryStream();
+ using var stream = new MemoryStream(buffer?.Length ?? 0);
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
- var newBytes = stream.ToArray();
+ byte[] newBytes = stream.GetBuffer();
+ int newBytesLen = (int)stream.Length;
// If the file didn't exist before, or if something has changed, re-save
- if (buffer == null || !buffer.SequenceEqual(newBytes))
+ if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
// Save it after load in case we got new items
- File.WriteAllBytes(path, newBytes);
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ fs.Write(newBytes, 0, newBytesLen);
+ }
}
return configuration;
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 18b6834ef..99530bfbd 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -43,10 +42,10 @@ using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
+using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
-using Emby.Server.Implementations.SyncPlay;
-using MediaBrowser.Api;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -78,8 +77,8 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Controller.TV;
using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.Model.Configuration;
@@ -97,7 +96,6 @@ using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles;
-using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@@ -192,7 +190,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
- protected ServerApplicationPaths ApplicationPaths { get; set; }
+ protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets or sets all concrete types.
@@ -236,7 +234,7 @@ namespace Emby.Server.Implementations
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// </summary>
public ApplicationHost(
- ServerApplicationPaths applicationPaths,
+ IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
@@ -484,12 +482,10 @@ namespace Emby.Server.Implementations
foreach (var plugin in Plugins)
{
- pluginBuilder.AppendLine(
- string.Format(
- CultureInfo.InvariantCulture,
- "{0} {1}",
- plugin.Name,
- plugin.Version));
+ pluginBuilder.Append(plugin.Name)
+ .Append(' ')
+ .Append(plugin.Version)
+ .AppendLine();
}
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
@@ -556,8 +552,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
-
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@@ -636,6 +630,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+
+ serviceCollection.AddSingleton<TranscodingJobHelper>();
+ serviceCollection.AddScoped<MediaInfoHelper>();
+ serviceCollection.AddScoped<AudioHelper>();
+ serviceCollection.AddScoped<DynamicHlsHelper>();
}
/// <summary>
@@ -652,7 +651,6 @@ namespace Emby.Server.Implementations
_httpServer = Resolve<IHttpServer>();
_httpClient = Resolve<IHttpClient>();
- ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();
@@ -797,7 +795,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
- Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
}
@@ -871,6 +868,11 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
continue;
}
+ catch (TypeLoadException ex)
+ {
+ Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
+ continue;
+ }
foreach (Type type in exportedTypes)
{
@@ -1032,12 +1034,6 @@ namespace Emby.Server.Implementations
}
}
- // Include composable parts in the Api assembly
- yield return typeof(ApiEntryPoint).Assembly;
-
- // Include composable parts in the Dashboard assembly
- yield return typeof(DashboardService).Assembly;
-
// Include composable parts in the Model assembly
yield return typeof(SystemInfo).Assembly;
@@ -1153,7 +1149,7 @@ namespace Emby.Server.Implementations
return null;
}
- return GetLocalApiUrl(addresses.First());
+ return GetLocalApiUrl(addresses[0]);
}
catch (Exception ex)
{
@@ -1226,7 +1222,7 @@ namespace Emby.Server.Implementations
var addresses = ServerConfigurationManager
.Configuration
.LocalNetworkAddresses
- .Select(NormalizeConfiguredLocalAddress)
+ .Select(x => NormalizeConfiguredLocalAddress(x))
.Where(i => i != null)
.ToList();
@@ -1247,8 +1243,7 @@ namespace Emby.Server.Implementations
}
}
- var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
- if (valid)
+ if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
{
resultList.Add(address);
@@ -1262,13 +1257,12 @@ namespace Emby.Server.Implementations
return resultList;
}
- public IPAddress NormalizeConfiguredLocalAddress(string address)
+ public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
{
var index = address.Trim('/').IndexOf('/');
-
if (index != -1)
{
- address = address.Substring(index + 1);
+ address = address.Slice(index + 1);
}
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
index 7a0294e07..f8108d1c2 100644
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
@@ -1,5 +1,7 @@
using System;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Browser
@@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
/// <param name="appHost">The app host.</param>
public static void OpenSwaggerPage(IServerApplicationHost appHost)
{
- TryOpenUrl(appHost, "/swagger/index.html");
+ TryOpenUrl(appHost, "/api-docs/swagger");
}
/// <summary>
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index c803d9d82..d8ab1f1a1 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -7,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
@@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
-
- private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
- new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
-
+ private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
/// <summary>
@@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
/// <param name="userDataManager">The user data manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param>
+ /// <param name="memoryCache">The memory cache.</param>
public ChannelManager(
IUserManager userManager,
IDtoService dtoService,
@@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem,
IUserDataManager userDataManager,
IJsonSerializer jsonSerializer,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IMemoryCache memoryCache)
{
_userManager = userManager;
_dtoService = dtoService;
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager;
+ _memoryCache = memoryCache;
}
internal IChannel[] Channels { get; private set; }
@@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
{
- if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
+ if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
{
- if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
- {
- return cachedInfo.Item2;
- }
+ return cachedInfo;
}
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
.ConfigureAwait(false);
var list = mediaInfo.ToList();
-
- var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
- _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
+ _memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
return list;
}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index ff7ee085f..64ccff53b 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Updates;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
@@ -19,7 +18,8 @@ namespace Emby.Server.Implementations
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
- { PlaylistsAllowDuplicatesKey, bool.TrueString }
+ { PlaylistsAllowDuplicatesKey, bool.TrueString },
+ { BindToUnixSocketKey, bool.FalseString }
};
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
deleted file mode 100644
index 5597155a8..000000000
--- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
+++ /dev/null
@@ -1,225 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text.Json;
-using System.Threading;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
- /// <summary>
- /// Class SQLiteDisplayPreferencesRepository.
- /// </summary>
- public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
- {
- private readonly IFileSystem _fileSystem;
-
- private readonly JsonSerializerOptions _jsonOptions;
-
- public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
- : base(logger)
- {
- _fileSystem = fileSystem;
-
- _jsonOptions = JsonDefaults.GetOptions();
-
- DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
- }
-
- /// <summary>
- /// Gets the name of the repository.
- /// </summary>
- /// <value>The name.</value>
- public string Name => "SQLite";
-
- public void Initialize()
- {
- try
- {
- InitializeInternal();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
- _fileSystem.DeleteFile(DbFilePath);
-
- InitializeInternal();
- }
- }
-
- /// <summary>
- /// Opens the connection to the database.
- /// </summary>
- /// <returns>Task.</returns>
- private void InitializeInternal()
- {
- string[] queries =
- {
- "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
- "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
- };
-
- using (var connection = GetConnection())
- {
- connection.RunQueries(queries);
- }
- }
-
- /// <summary>
- /// Save the display preferences associated with an item in the repo.
- /// </summary>
- /// <param name="displayPreferences">The display preferences.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="client">The client.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">item</exception>
- public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
- {
- if (displayPreferences == null)
- {
- throw new ArgumentNullException(nameof(displayPreferences));
- }
-
- if (string.IsNullOrEmpty(displayPreferences.Id))
- {
- throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db => SaveDisplayPreferences(displayPreferences, userId, client, db),
- TransactionMode);
- }
- }
-
- private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
- {
- var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
-
- using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
- {
- statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
- statement.TryBind("@userId", userId.ToByteArray());
- statement.TryBind("@client", client);
- statement.TryBind("@data", serialized);
-
- statement.MoveNext();
- }
- }
-
- /// <summary>
- /// Save all display preferences associated with a user in the repo.
- /// </summary>
- /// <param name="displayPreferences">The display preferences.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">item</exception>
- public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
- {
- if (displayPreferences == null)
- {
- throw new ArgumentNullException(nameof(displayPreferences));
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- foreach (var displayPreference in displayPreferences)
- {
- SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
- }
- },
- TransactionMode);
- }
- }
-
- /// <summary>
- /// Gets the display preferences.
- /// </summary>
- /// <param name="displayPreferencesId">The display preferences id.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="client">The client.</param>
- /// <returns>Task{DisplayPreferences}.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
- {
- if (string.IsNullOrEmpty(displayPreferencesId))
- {
- throw new ArgumentNullException(nameof(displayPreferencesId));
- }
-
- var guidId = displayPreferencesId.GetMD5();
-
- using (var connection = GetConnection(true))
- {
- using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
- {
- statement.TryBind("@id", guidId.ToByteArray());
- statement.TryBind("@userId", userId.ToByteArray());
- statement.TryBind("@client", client);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return Get(row);
- }
- }
- }
-
- return new DisplayPreferences
- {
- Id = guidId.ToString("N", CultureInfo.InvariantCulture)
- };
- }
-
- /// <summary>
- /// Gets all display preferences for the given user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <returns>Task{DisplayPreferences}.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
- {
- var list = new List<DisplayPreferences>();
-
- using (var connection = GetConnection(true))
- using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
- {
- statement.TryBind("@userId", userId.ToByteArray());
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(Get(row));
- }
- }
-
- return list;
- }
-
- private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
- => JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
-
- public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
- => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
-
- public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
- => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index a6390b1ef..331ffc134 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -9,6 +9,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
@@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
"OwnerId"
};
+ private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
@@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
"ColorTransfer"
};
+ private static readonly string _mediaStreamSaveColumnsInsertQuery =
+ $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
+
+ private static readonly string _mediaStreamSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
+
private static readonly string[] _mediaAttachmentSaveColumns =
{
"ItemId",
@@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
"MIMEType"
};
- private static readonly string _mediaAttachmentInsertPrefix;
-
- private static string GetSaveItemCommandText()
- {
- var saveColumns = new[]
- {
- "guid",
- "type",
- "data",
- "Path",
- "StartDate",
- "EndDate",
- "ChannelId",
- "IsMovie",
- "IsSeries",
- "EpisodeTitle",
- "IsRepeat",
- "CommunityRating",
- "CustomRating",
- "IndexNumber",
- "IsLocked",
- "Name",
- "OfficialRating",
- "MediaType",
- "Overview",
- "ParentIndexNumber",
- "PremiereDate",
- "ProductionYear",
- "ParentId",
- "Genres",
- "InheritedParentalRatingValue",
- "SortName",
- "ForcedSortName",
- "RunTimeTicks",
- "Size",
- "DateCreated",
- "DateModified",
- "PreferredMetadataLanguage",
- "PreferredMetadataCountryCode",
- "Width",
- "Height",
- "DateLastRefreshed",
- "DateLastSaved",
- "IsInMixedFolder",
- "LockedFields",
- "Studios",
- "Audio",
- "ExternalServiceId",
- "Tags",
- "IsFolder",
- "UnratedType",
- "TopParentId",
- "TrailerTypes",
- "CriticRating",
- "CleanName",
- "PresentationUniqueKey",
- "OriginalTitle",
- "PrimaryVersionId",
- "DateLastMediaAdded",
- "Album",
- "IsVirtualItem",
- "SeriesName",
- "UserDataKey",
- "SeasonName",
- "SeasonId",
- "SeriesId",
- "ExternalSeriesId",
- "Tagline",
- "ProviderIds",
- "Images",
- "ProductionLocations",
- "ExtraIds",
- "TotalBitrate",
- "ExtraType",
- "Artists",
- "AlbumArtists",
- "ExternalId",
- "SeriesPresentationUniqueKey",
- "ShowId",
- "OwnerId"
- };
-
- var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
+ private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
- for (var i = 0; i < saveColumns.Length; i++)
- {
- if (i != 0)
- {
- saveItemCommandCommandText += ",";
- }
-
- saveItemCommandCommandText += "@" + saveColumns[i];
- }
+ private static readonly string _mediaAttachmentInsertPrefix;
- return saveItemCommandCommandText + ")";
- }
+ private const string SaveItemCommandText =
+ @"replace into TypedBaseItems
+ (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+ values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
/// <summary>
/// Save a standard item in the repo.
@@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
{
var statements = PrepareAll(db, new string[]
{
- GetSaveItemCommandText(),
+ SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
}).ToList();
@@ -1056,7 +978,10 @@ namespace Emby.Server.Implementations.Data
continue;
}
- str.Append($"{i.Key}={i.Value}|");
+ str.Append(i.Key)
+ .Append('=')
+ .Append(i.Value)
+ .Append('|');
}
if (str.Length == 0)
@@ -1110,7 +1035,8 @@ namespace Emby.Server.Implementations.Data
continue;
}
- str.Append(ToValueString(i) + "|");
+ AppendItemImageInfo(str, i);
+ str.Append('|');
}
str.Length -= 1; // Remove last |
@@ -1144,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
item.ImageInfos = list.ToArray();
}
- public string ToValueString(ItemImageInfo image)
+ public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
{
- const string Delimeter = "*";
+ const char Delimiter = '*';
var path = image.Path ?? string.Empty;
var hash = image.BlurHash ?? string.Empty;
- return GetPathToSave(path) +
- Delimeter +
- image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
- Delimeter +
- image.Type +
- Delimeter +
- image.Width.ToString(CultureInfo.InvariantCulture) +
- Delimeter +
- image.Height.ToString(CultureInfo.InvariantCulture) +
- Delimeter +
- // Replace delimiters with other characters.
- // This can be removed when we migrate to a proper DB.
- hash.Replace('*', '/').Replace('|', '\\');
+ bldr.Append(GetPathToSave(path))
+ .Append(Delimiter)
+ .Append(image.DateModified.Ticks)
+ .Append(Delimiter)
+ .Append(image.Type)
+ .Append(Delimiter)
+ .Append(image.Width)
+ .Append(Delimiter)
+ .Append(image.Height)
+ .Append(Delimiter)
+ // Replace delimiters with other characters.
+ // This can be removed when we migrate to a proper DB.
+ .Append(hash.Replace('*', '/').Replace('|', '\\'));
}
public ItemImageInfo ItemImageInfoFromValueString(string value)
@@ -1225,7 +1151,7 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection(true))
{
- using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
+ using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -2471,7 +2397,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo;
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating))
{
@@ -2509,7 +2435,7 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(query.SearchTerm))
{
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
@@ -2775,82 +2701,82 @@ namespace Emby.Server.Implementations.Data
private string FixUnicodeChars(string buffer)
{
- if (buffer.IndexOf('\u2013') > -1)
+ if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2013', '-'); // en dash
}
- if (buffer.IndexOf('\u2014') > -1)
+ if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2014', '-'); // em dash
}
- if (buffer.IndexOf('\u2015') > -1)
+ if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
}
- if (buffer.IndexOf('\u2017') > -1)
+ if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2017', '_'); // double low line
}
- if (buffer.IndexOf('\u2018') > -1)
+ if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
}
- if (buffer.IndexOf('\u2019') > -1)
+ if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
}
- if (buffer.IndexOf('\u201a') > -1)
+ if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
}
- if (buffer.IndexOf('\u201b') > -1)
+ if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
}
- if (buffer.IndexOf('\u201c') > -1)
+ if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
}
- if (buffer.IndexOf('\u201d') > -1)
+ if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
}
- if (buffer.IndexOf('\u201e') > -1)
+ if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
}
- if (buffer.IndexOf('\u2026') > -1)
+ if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
{
- buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
+ buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
}
- if (buffer.IndexOf('\u2032') > -1)
+ if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2032', '\''); // prime
}
- if (buffer.IndexOf('\u2033') > -1)
+ if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2033', '\"'); // double prime
}
- if (buffer.IndexOf('\u0060') > -1)
+ if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u0060', '\''); // grave accent
}
- if (buffer.IndexOf('\u00B4') > -1)
+ if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u00B4', '\''); // acute accent
}
@@ -2999,7 +2925,6 @@ namespace Emby.Server.Implementations.Data
{
connection.RunInTransaction(db =>
{
-
var statements = PrepareAll(db, statementTexts).ToList();
if (!isReturningZeroItems)
@@ -4635,13 +4560,13 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length > 1)
{
var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
+ whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
}
if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
{
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
- whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
+ whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
if (statement != null)
{
statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
@@ -4669,8 +4594,12 @@ namespace Emby.Server.Implementations.Data
if (query.BlockUnratedItems.Length > 1)
{
- var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
- whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
+ var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
+ whereClauses.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
+ inClause));
}
if (query.ExcludeInheritedTags.Length > 0)
@@ -4679,7 +4608,7 @@ namespace Emby.Server.Implementations.Data
if (statement == null)
{
int index = 0;
- string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
+ string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
}
else
@@ -5238,7 +5167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
if (i > 0)
{
- insertText.Append(",");
+ insertText.Append(',');
}
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
@@ -5733,10 +5662,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 100;
var startIndex = 0;
+ const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
+ var insertText = new StringBuilder(StartInsertText);
while (startIndex < values.Count)
{
- var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
-
var endIndex = Math.Min(values.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
@@ -5778,6 +5707,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
+ insertText.Length = StartInsertText.Length;
}
}
@@ -5815,10 +5745,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var startIndex = 0;
var listIndex = 0;
+ const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
+ var insertText = new StringBuilder(StartInsertText);
while (startIndex < people.Count)
{
- var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
-
var endIndex = Math.Min(people.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5852,6 +5782,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
+ insertText.Length = StartInsertText.Length;
}
}
@@ -5890,10 +5821,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query));
}
- var cmdText = "select "
- + string.Join(",", _mediaStreamSaveColumns)
- + " from mediastreams where"
- + " ItemId=@ItemId";
+ var cmdText = _mediaStreamSaveColumnsSelectQuery;
if (query.Type.HasValue)
{
@@ -5970,18 +5898,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 10;
var startIndex = 0;
+ var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
while (startIndex < streams.Count)
{
- var insertText = new StringBuilder("insert into mediastreams (");
- foreach (var column in _mediaStreamSaveColumns)
- {
- insertText.Append(column).Append(',');
- }
-
- // Remove last comma
- insertText.Length--;
- insertText.Append(") values ");
-
var endIndex = Math.Min(streams.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
@@ -6064,6 +5983,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
+ insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
}
}
@@ -6247,10 +6167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query));
}
- var cmdText = "select "
- + string.Join(",", _mediaAttachmentSaveColumns)
- + " from mediaattachments where"
- + " ItemId=@ItemId";
+ var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
if (query.Index.HasValue)
{
@@ -6318,10 +6235,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
const int InsertAtOnce = 10;
+ var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
{
- var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
-
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
for (var i = startIndex; i < endIndex; i++)
@@ -6331,7 +6247,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{
- insertText.Append("@" + column + index + ",");
+ insertText.Append('@')
+ .Append(column)
+ .Append(index)
+ .Append(',');
}
insertText.Length -= 1;
@@ -6364,6 +6283,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statement.Reset();
statement.MoveNext();
}
+
+ insertText.Length = _mediaAttachmentInsertPrefix.Length;
}
}
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index e75745cc6..cc4b407f5 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -5,8 +5,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using Jellyfin.Data.Enums;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
+ private readonly IMemoryCache _memoryCache;
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo;
- private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
private readonly object _capabilitiesSyncLock = new object();
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
@@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
IAuthenticationRepository authRepo,
IJsonSerializer json,
IUserManager userManager,
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ IMemoryCache memoryCache)
{
_json = json;
_userManager = userManager;
_config = config;
+ _memoryCache = memoryCache;
_authRepo = authRepo;
- _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
}
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
@@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
lock (_capabilitiesSyncLock)
{
- _capabilitiesCache[deviceId] = capabilities;
-
+ _memoryCache.Set(deviceId, capabilities);
_json.SerializeToFile(capabilities, path);
}
}
@@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id)
{
- lock (_capabilitiesSyncLock)
+ if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
{
- if (_capabilitiesCache.TryGetValue(id, out var result))
- {
- return result;
- }
+ return result;
+ }
+ lock (_capabilitiesSyncLock)
+ {
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
try
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index c967e9230..f2c7118fe 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -73,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
- /// <summary>
- /// Converts a BaseItem to a DTOBaseItem.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="fields">The fields.</param>
- /// <param name="user">The user.</param>
- /// <param name="owner">The owner.</param>
- /// <returns>Task{DtoBaseItem}.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null)
- {
- var options = new DtoOptions
- {
- Fields = fields
- };
-
- return GetBaseItemDto(item, options, user, owner);
- }
-
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@@ -443,17 +424,6 @@ namespace Emby.Server.Implementations.Dto
return folder.GetChildCount(user);
}
- /// <summary>
- /// Gets client-side Id of a server-side BaseItem.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public string GetDtoId(BaseItem item)
- {
- return item.Id.ToString("N", CultureInfo.InvariantCulture);
- }
-
private static void SetBookProperties(BaseItemDto dto, Book item)
{
dto.SeriesName = item.SeriesName;
@@ -484,6 +454,11 @@ namespace Emby.Server.Implementations.Dto
}
}
+ private string GetDtoId(BaseItem item)
+ {
+ return item.Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+
private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
{
if (!string.IsNullOrEmpty(item.Album))
@@ -513,19 +488,6 @@ namespace Emby.Server.Implementations.Dto
.ToArray();
}
- private string GetImageCacheTag(BaseItem item, ImageType type)
- {
- try
- {
- return _imageProcessor.GetImageCacheTag(item, type);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting {type} image info", type);
- return null;
- }
- }
-
private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
try
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index e75b66293..1adef68aa 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -13,10 +13,8 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
- <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
- <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
@@ -25,7 +23,7 @@
<ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.5.211" />
- <PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" />
+ <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
@@ -34,14 +32,14 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
- <PackageReference Include="Mono.Nat" Version="2.0.1" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Mono.Nat" Version="2.0.2" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
- <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
- <PackageReference Include="sharpcompress" Version="0.25.1" />
+ <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
+ <PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
</ItemGroup>
@@ -54,7 +52,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+ <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index c1068522a..1deef7f72 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -23,10 +23,12 @@ namespace Emby.Server.Implementations.EntryPoints
public class LibraryChangedNotifier : IServerEntryPoint
{
/// <summary>
- /// The library manager.
+ /// The library update duration.
/// </summary>
- private readonly ILibraryManager _libraryManager;
+ private const int LibraryUpdateDuration = 30000;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
private readonly ILogger<LibraryChangedNotifier> _logger;
@@ -38,23 +40,10 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly List<Folder> _foldersAddedTo = new List<Folder>();
private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
-
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
-
- /// <summary>
- /// Gets or sets the library update timer.
- /// </summary>
- /// <value>The library update timer.</value>
- private Timer LibraryUpdateTimer { get; set; }
-
- /// <summary>
- /// The library update duration.
- /// </summary>
- private const int LibraryUpdateDuration = 30000;
-
- private readonly IProviderManager _providerManager;
+ private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
public LibraryChangedNotifier(
ILibraryManager libraryManager,
@@ -70,22 +59,26 @@ namespace Emby.Server.Implementations.EntryPoints
_providerManager = providerManager;
}
+ /// <summary>
+ /// Gets or sets the library update timer.
+ /// </summary>
+ /// <value>The library update timer.</value>
+ private Timer LibraryUpdateTimer { get; set; }
+
public Task RunAsync()
{
- _libraryManager.ItemAdded += libraryManager_ItemAdded;
- _libraryManager.ItemUpdated += libraryManager_ItemUpdated;
- _libraryManager.ItemRemoved += libraryManager_ItemRemoved;
+ _libraryManager.ItemAdded += OnLibraryItemAdded;
+ _libraryManager.ItemUpdated += OnLibraryItemUpdated;
+ _libraryManager.ItemRemoved += OnLibraryItemRemoved;
- _providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
- _providerManager.RefreshStarted += _providerManager_RefreshStarted;
- _providerManager.RefreshProgress += _providerManager_RefreshProgress;
+ _providerManager.RefreshCompleted += OnProviderRefreshCompleted;
+ _providerManager.RefreshStarted += OnProviderRefreshStarted;
+ _providerManager.RefreshProgress += OnProviderRefreshProgress;
return Task.CompletedTask;
}
- private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
-
- private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
+ private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
{
var item = e.Argument.Item1;
@@ -122,9 +115,11 @@ namespace Emby.Server.Implementations.EntryPoints
foreach (var collectionFolder in collectionFolders)
{
- var collectionFolderDict = new Dictionary<string, string>();
- collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
- collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
+ var collectionFolderDict = new Dictionary<string, string>
+ {
+ ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
+ ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
+ };
try
{
@@ -136,21 +131,19 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
+ private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
{
- _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
+ OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
}
- private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
+ private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
{
- _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
+ OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
}
private static bool EnableRefreshMessage(BaseItem item)
{
- var folder = item as Folder;
-
- if (folder == null)
+ if (!(item is Folder folder))
{
return false;
}
@@ -183,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+ private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -205,8 +198,7 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
}
- var parent = e.Item.GetParent() as Folder;
- if (parent != null)
+ if (e.Item.GetParent() is Folder parent)
{
_foldersAddedTo.Add(parent);
}
@@ -220,7 +212,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
+ private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -231,8 +223,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer == null)
{
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
- Timeout.Infinite);
+ LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
else
{
@@ -248,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
+ private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -259,16 +250,14 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer == null)
{
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
- Timeout.Infinite);
+ LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
else
{
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
}
- var parent = e.Parent as Folder;
- if (parent != null)
+ if (e.Parent is Folder parent)
{
_foldersRemovedFrom.Add(parent);
}
@@ -486,13 +475,13 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer = null;
}
- _libraryManager.ItemAdded -= libraryManager_ItemAdded;
- _libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
- _libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
+ _libraryManager.ItemAdded -= OnLibraryItemAdded;
+ _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
+ _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
- _providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
- _providerManager.RefreshStarted -= _providerManager_RefreshStarted;
- _providerManager.RefreshProgress -= _providerManager_RefreshProgress;
+ _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
+ _providerManager.RefreshStarted -= OnProviderRefreshStarted;
+ _providerManager.RefreshProgress -= OnProviderRefreshProgress;
}
}
}
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index b207397bd..9486874d5 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,3 +1,4 @@
+using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
@@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc />
public Task RunAsync()
{
- _udpServer = new UdpServer(_logger, _appHost, _config);
- _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ try
+ {
+ _udpServer = new UdpServer(_logger, _appHost, _config);
+ _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+ }
+
return Task.CompletedTask;
}
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs
index 590eee1b4..6fce8de44 100644
--- a/Emby.Server.Implementations/HttpServer/FileWriter.cs
+++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs
@@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
private readonly IStreamHelper _streamHelper;
private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
/// <summary>
/// The _options.
@@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
}
_streamHelper = streamHelper;
- _fileSystem = fileSystem;
Path = path;
_logger = logger;
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index c3428ee62..dafdd5b7b 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
- foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+ foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
{
httpRes.Headers.Add(key, value);
}
@@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
var handler = GetServiceHandler(httpReq);
if (handler != null)
{
- await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
+ await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
}
else
{
@@ -567,7 +567,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
- var connection = new WebSocketConnection(
+ using var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
context.Connection.RemoteIpAddress,
diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
index 970f5119c..688216373 100644
--- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
@@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>();
}
- if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
+ if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
@@ -326,7 +326,8 @@ namespace Emby.Server.Implementations.HttpServer
return GetHttpResult(request, ms, contentType, true, responseHeaders);
}
- private IHasHeaders GetCompressedResult(byte[] content,
+ private IHasHeaders GetCompressedResult(
+ byte[] content,
string requestedCompressionType,
IDictionary<string, string> responseHeaders,
bool isHeadRequest,
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 318bc6a24..76c1d9bac 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -13,35 +13,31 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer.Security
{
public class AuthService : IAuthService
{
- private readonly ILogger<AuthService> _logger;
private readonly IAuthorizationContext _authorizationContext;
private readonly ISessionManager _sessionManager;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
public AuthService(
- ILogger<AuthService> logger,
IAuthorizationContext authorizationContext,
IServerConfigurationManager config,
ISessionManager sessionManager,
INetworkManager networkManager)
{
- _logger = logger;
_authorizationContext = authorizationContext;
_config = config;
_sessionManager = sessionManager;
_networkManager = networkManager;
}
- public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
+ public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{
- ValidateUser(request, authAttribtues);
+ ValidateUser(request, authAttributes);
}
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@@ -67,17 +63,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
return auth;
}
- private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
+ private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{
// This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
- if (!IsExemptFromAuthenticationToken(authAttribtues, request))
+ if (!IsExemptFromAuthenticationToken(authAttributes, request))
{
ValidateSecurityToken(request, auth.Token);
}
- if (authAttribtues.AllowLocalOnly && !request.IsLocal)
+ if (authAttributes.AllowLocalOnly && !request.IsLocal)
{
throw new SecurityException("Operation not found.");
}
@@ -91,14 +87,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user != null)
{
- ValidateUserAccess(user, request, authAttribtues, auth);
+ ValidateUserAccess(user, request, authAttributes);
}
var info = GetTokenInfo(request);
- if (!IsExemptFromRoles(auth, authAttribtues, request, info))
+ if (!IsExemptFromRoles(auth, authAttributes, request, info))
{
- var roles = authAttribtues.GetRoles();
+ var roles = authAttributes.GetRoles();
ValidateRoles(roles, user);
}
@@ -122,8 +118,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess(
User user,
IRequest request,
- IAuthenticationAttributes authAttributes,
- AuthorizationInfo auth)
+ IAuthenticationAttributes authAttributes)
{
if (user.HasPermission(PermissionKind.IsDisabled))
{
@@ -162,6 +157,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
return true;
}
+ if (authAttribtues.IgnoreLegacyAuth)
+ {
+ return true;
+ }
+
return false;
}
@@ -241,16 +241,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
throw new AuthenticationException("Access token is invalid or expired.");
}
-
- // if (!string.IsNullOrEmpty(info.UserId))
- //{
- // var user = _userManager.GetUserById(info.UserId);
-
- // if (user == null || user.Configuration.IsDisabled)
- // {
- // throw new SecurityException("User account has been disabled.");
- // }
- //}
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 078ce0d8a..fb93fae3e 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -99,6 +99,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token))
{
+ token = queryString["ApiKey"];
+ }
+
+ // TODO deprecate this query parameter.
+ if (string.IsNullOrEmpty(token))
+ {
token = queryString["api_key"];
}
@@ -276,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private static string NormalizeValue(string value)
{
- if (string.IsNullOrEmpty(value))
- {
- return value;
- }
-
- return WebUtility.HtmlEncode(value);
+ return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs
index 5afc51dbc..00e3ab8fe 100644
--- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs
+++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs
@@ -95,13 +95,13 @@ namespace Emby.Server.Implementations.HttpServer
if (bytes != null)
{
- await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
}
else
{
using (var src = SourceStream)
{
- await src.CopyToAsync(responseStream).ConfigureAwait(false);
+ await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 316cd84cf..d738047e0 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Class WebSocketConnection.
/// </summary>
- public class WebSocketConnection : IWebSocketConnection
+ public class WebSocketConnection : IWebSocketConnection, IDisposable
{
/// <summary>
/// The logger.
@@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.HttpServer
Memory<byte> memory = writer.GetMemory(512);
try
{
- receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+ receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
}
catch (WebSocketException ex)
{
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
writer.Advance(bytesRead);
// Make the data available to the PipeReader
- FlushResult flushResult = await writer.FlushAsync();
+ FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
if (flushResult.IsCompleted)
{
// The PipeReader stopped reading
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
{
- await SendKeepAliveResponse();
+ await SendKeepAliveResponse().ConfigureAwait(false);
}
else
{
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index ef93779aa..fe74f1de7 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object();
private Timer _timer;
+ private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
{
@@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
}
}
- private bool _disposed;
public void Dispose()
{
_disposed = true;
DisposeTimer();
+ GC.SuppressFinalize(this);
}
}
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index a3a3f91b7..ab6483bf9 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
if (info is FileInfo fileInfo)
{
result.Length = fileInfo.Length;
+
+ // Issue #2354 get the size of files behind symbolic links
+ if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ {
+ using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ {
+ result.Length = thisFileStream.Length;
+ }
+ }
+
result.DirectoryName = fileInfo.DirectoryName;
}
diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
index 52896720e..bf57382ed 100644
--- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
/// </summary>
public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
{
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
- _libraryManager = libraryManager;
}
/// <summary>
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index da88b8d8a..161b4c452 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index e9523386e..0224ab32a 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index d2aeccdb2..1cd4cd66b 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 77b2c0a69..3380e29d4 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
+ if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index ab39a7223..236453e80 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
{
public class ExclusiveLiveStream : ILiveStream
{
+ private readonly Func<Task> _closeFn;
+
+ public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
+ {
+ MediaSource = mediaSource;
+ EnableStreamSharing = false;
+ _closeFn = closeFn;
+ ConsumerCount = 1;
+ UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ }
+
public int ConsumerCount { get; set; }
public string OriginalStreamId { get; set; }
@@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
public MediaSourceInfo MediaSource { get; set; }
- public string UniqueId { get; private set; }
-
- private Func<Task> _closeFn;
-
- public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
- {
- MediaSource = mediaSource;
- EnableStreamSharing = false;
- _closeFn = closeFn;
- ConsumerCount = 1;
- UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- }
+ public string UniqueId { get; }
public Task Close()
{
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 6e6ef1359..e30a67593 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
{
"**/small.jpg",
"**/albumart.jpg",
- "**/*sample*",
+
+ // We have neither non-greedy matching or character group repetitions, working around that here.
+ // https://github.com/dazinator/DotNet.Glob#patterns
+ // .*/sample\..{1,5}
+ "**/sample.?",
+ "**/sample.??",
+ "**/sample.???", // Matches sample.mkv
+ "**/sample.????", // Matches sample.webm
+ "**/sample.?????",
+ "**/*.sample.?",
+ "**/*.sample.??",
+ "**/*.sample.???",
+ "**/*.sample.????",
+ "**/*.sample.?????",
+ "**/sample/*",
// Directories
"**/metadata/**",
@@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
"**/.grab/**",
"**/.grab",
- // Unix hidden files and directories
- "**/.*/**",
+ // Unix hidden files
"**/.*",
+ // Mac - if you ever remove the above.
+ // "**/._*",
+ // "**/.DS_Store",
+
// thumbs.db
"**/thumbs.db",
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 77d44e131..7b770d940 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
-using SortOrder = MediaBrowser.Model.Entities.SortOrder;
using VideoResolver = Emby.Naming.Video.VideoResolver;
namespace Emby.Server.Implementations.Library
@@ -60,7 +59,10 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class LibraryManager : ILibraryManager
{
+ private const string ShortcutFileExtension = ".mblink";
+
private readonly ILogger<LibraryManager> _logger;
+ private readonly IMemoryCache _memoryCache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -72,66 +74,26 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
- private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor;
- private NamingOptions _namingOptions;
- private string[] _videoFileExtensions;
-
- private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
-
- private IProviderManager ProviderManager => _providerManagerFactory.Value;
-
- private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
-
- /// <summary>
- /// Gets or sets the postscan tasks.
- /// </summary>
- /// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; }
-
- /// <summary>
- /// Gets or sets the intro providers.
- /// </summary>
- /// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; }
-
- /// <summary>
- /// Gets or sets the list of entity resolution ignore rules.
- /// </summary>
- /// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
-
- /// <summary>
- /// Gets or sets the list of currently registered entity resolvers.
- /// </summary>
- /// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; }
-
- private IMultiItemResolver[] MultiItemResolvers { get; set; }
-
/// <summary>
- /// Gets or sets the comparers.
+ /// The _root folder sync lock.
/// </summary>
- /// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; }
+ private readonly object _rootFolderSyncLock = new object();
+ private readonly object _userRootFolderSyncLock = new object();
- /// <summary>
- /// Occurs when [item added].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemAdded;
+ private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
- /// <summary>
- /// Occurs when [item updated].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ private NamingOptions _namingOptions;
+ private string[] _videoFileExtensions;
/// <summary>
- /// Occurs when [item removed].
+ /// The _root folder.
/// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+ private volatile AggregateFolder _rootFolder;
+ private volatile UserRootFolder _userRootFolder;
- public bool IsScanRunning { get; private set; }
+ private bool _wizardCompleted;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
@@ -149,6 +111,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
+ /// <param name="memoryCache">The memory cache.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILogger<LibraryManager> logger,
@@ -162,7 +125,8 @@ namespace Emby.Server.Implementations.Library
Lazy<IUserViewManager> userviewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
- IImageProcessor imageProcessor)
+ IImageProcessor imageProcessor,
+ IMemoryCache memoryCache)
{
_appHost = appHost;
_logger = logger;
@@ -177,8 +141,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
-
- _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
+ _memoryCache = memoryCache;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Adds the parts.
+ /// Occurs when [item added].
/// </summary>
- /// <param name="rules">The rules.</param>
- /// <param name="resolvers">The resolvers.</param>
- /// <param name="introProviders">The intro providers.</param>
- /// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The post scan tasks.</param>
- public void AddParts(
- IEnumerable<IResolverIgnoreRule> rules,
- IEnumerable<IItemResolver> resolvers,
- IEnumerable<IIntroProvider> introProviders,
- IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks)
- {
- EntityResolutionIgnoreRules = rules.ToArray();
- EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
- MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
- IntroProviders = introProviders.ToArray();
- Comparers = itemComparers.ToArray();
- PostscanTasks = postscanTasks.ToArray();
- }
+ public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <summary>
- /// The _root folder.
+ /// Occurs when [item updated].
/// </summary>
- private volatile AggregateFolder _rootFolder;
+ public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary>
- /// The _root folder sync lock.
+ /// Occurs when [item removed].
/// </summary>
- private readonly object _rootFolderSyncLock = new object();
+ public event EventHandler<ItemChangeEventArgs> ItemRemoved;
/// <summary>
/// Gets the root folder.
@@ -241,7 +186,68 @@ namespace Emby.Server.Implementations.Library
}
}
- private bool _wizardCompleted;
+ private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+ private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+ private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
+ /// <summary>
+ /// Gets or sets the postscan tasks.
+ /// </summary>
+ /// <value>The postscan tasks.</value>
+ private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the intro providers.
+ /// </summary>
+ /// <value>The intro providers.</value>
+ private IIntroProvider[] IntroProviders { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of entity resolution ignore rules.
+ /// </summary>
+ /// <value>The entity resolution ignore rules.</value>
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of currently registered entity resolvers.
+ /// </summary>
+ /// <value>The entity resolvers enumerable.</value>
+ private IItemResolver[] EntityResolvers { get; set; }
+
+ private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comparers.
+ /// </summary>
+ /// <value>The comparers.</value>
+ private IBaseItemComparer[] Comparers { get; set; }
+
+ public bool IsScanRunning { get; private set; }
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="rules">The rules.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <param name="introProviders">The intro providers.</param>
+ /// <param name="itemComparers">The item comparers.</param>
+ /// <param name="postscanTasks">The post scan tasks.</param>
+ public void AddParts(
+ IEnumerable<IResolverIgnoreRule> rules,
+ IEnumerable<IItemResolver> resolvers,
+ IEnumerable<IIntroProvider> introProviders,
+ IEnumerable<IBaseItemComparer> itemComparers,
+ IEnumerable<ILibraryPostScanTask> postscanTasks)
+ {
+ EntityResolutionIgnoreRules = rules.ToArray();
+ EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+ MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+ IntroProviders = introProviders.ToArray();
+ Comparers = itemComparers.ToArray();
+ PostscanTasks = postscanTasks.ToArray();
+ }
/// <summary>
/// Records the configuration values.
@@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
+ _memoryCache.Set(item.Id, item);
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -341,7 +347,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram)
{
_logger.LogDebug(
- "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -350,7 +356,7 @@ namespace Emby.Server.Implementations.Library
else
{
_logger.LogInformation(
- "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -368,7 +374,12 @@ namespace Emby.Server.Implementations.Library
continue;
}
- _logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
+ _logger.LogDebug(
+ "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ metadataPath,
+ item.Id);
try
{
@@ -392,7 +403,13 @@ namespace Emby.Server.Implementations.Library
{
try
{
- _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
+ _logger.LogInformation(
+ "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ fileSystemInfo.FullName,
+ item.Id);
+
if (fileSystemInfo.IsDirectory)
{
Directory.Delete(fileSystemInfo.FullName, true);
@@ -430,7 +447,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id);
}
- _libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
+ _memoryCache.Remove(item.Id);
ReportItemRemoved(item, parent);
}
@@ -500,8 +517,8 @@ namespace Emby.Server.Implementations.Library
{
// Try to normalize paths located underneath program-data in an attempt to make them more portable
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
- .TrimStart(new[] { '/', '\\' })
- .Replace("/", "\\");
+ .TrimStart('/', '\\')
+ .Replace('/', '\\');
}
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@@ -764,14 +781,11 @@ namespace Emby.Server.Implementations.Library
return rootFolder;
}
- private volatile UserRootFolder _userRootFolder;
- private readonly object _syncLock = new object();
-
public Folder GetUserRootFolder()
{
if (_userRootFolder == null)
{
- lock (_syncLock)
+ lock (_userRootFolderSyncLock)
{
if (_userRootFolder == null)
{
@@ -1234,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
+ if (_memoryCache.TryGetValue(id, out BaseItem item))
{
return item;
}
@@ -1321,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
return new QueryResult<BaseItem>
{
- Items = _itemRepository.GetItemList(query).ToArray()
+ Items = _itemRepository.GetItemList(query)
};
}
@@ -1452,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItems(query);
}
- var list = _itemRepository.GetItemList(query);
-
return new QueryResult<BaseItem>
{
- Items = list
+ Items = _itemRepository.GetItemList(query)
};
}
@@ -1579,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{
var tasks = IntroProviders
- .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
.Take(1)
.Select(i => GetIntros(i, item, user));
@@ -1865,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
}
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
- if (outdated.Length == 0)
+ // Skip image processing if current or live tv source
+ if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{
RegisterItem(item);
return;
@@ -1934,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Updates the item.
/// </summary>
- public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
- // Don't iterate multiple times
- var itemsList = items.ToList();
-
- foreach (var item in itemsList)
+ foreach (var item in items)
{
if (item.IsFileProtocol)
{
@@ -1951,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
}
- _itemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null)
{
- foreach (var item in itemsList)
+ foreach (var item in items)
{
// With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library)
@@ -2178,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
}
- private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
public UserView GetNamedView(
User user,
string name,
@@ -2477,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
- var episodeInfo = episode.IsFileProtocol ?
- resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
- new Naming.TV.EpisodeInfo();
-
- if (episodeInfo == null)
- {
- episodeInfo = new Naming.TV.EpisodeInfo();
- }
+ var episodeInfo = episode.IsFileProtocol
+ ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
+ : new Naming.TV.EpisodeInfo();
try
{
@@ -2492,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{
// Read from metadata
- var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = episode.GetMediaSources(false)[0],
- MediaType = DlnaProfileType.Video
- }, CancellationToken.None).GetAwaiter().GetResult();
+ var mediaInfo = _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = episode.GetMediaSources(false)[0],
+ MediaType = DlnaProfileType.Video
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
if (mediaInfo.ParentIndexNumber > 0)
{
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2654,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
var videos = videoListResolver.Resolve(fileSystemChildren);
- var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+ var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null)
{
@@ -2671,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
.Select(video =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
- var dbItem = GetItemById(video.Id) as Trailer;
-
- if (dbItem != null)
+ if (GetItemById(video.Id) is Trailer dbItem)
{
video = dbItem;
}
@@ -3000,23 +3002,6 @@ namespace Emby.Server.Implementations.Library
});
}
- private static bool ValidateNetworkPath(string path)
- {
- // if (Environment.OSVersion.Platform == PlatformID.Win32NT)
- //{
- // // We can't validate protocol-based paths, so just allow them
- // if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
- // {
- // return Directory.Exists(path);
- // }
- //}
-
- // Without native support for unc, we cannot validate this when running under mono
- return true;
- }
-
- private const string ShortcutFileExtension = ".mblink";
-
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{
AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -3041,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The path does not exist.");
}
- if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
- {
- throw new FileNotFoundException("The network path does not exist.");
- }
-
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@@ -3084,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(pathInfo));
}
- if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
- {
- throw new FileNotFoundException("The network path does not exist.");
- }
-
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@@ -3220,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
if (!Directory.Exists(virtualFolderPath))
{
- throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+ throw new FileNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
}
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 9b9f53049..041619d1e 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
-
- private IJsonSerializer _json;
- private IApplicationPaths _appPaths;
+ private readonly IJsonSerializer _json;
+ private readonly IApplicationPaths _appPaths;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
{
@@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000;
- mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = mediaSource,
- MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
- ExtractChapters = false
-
- }, cancellationToken).ConfigureAwait(false);
+ mediaInfo = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false
+ },
+ cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null)
{
@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.RunTimeTicks = null;
}
- var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+ var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1)
{
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
}
- var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+ var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null)
{
if (!videoStream.BitRate.HasValue)
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index ceb36b389..67cf8bf5b 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
{
public class MediaSourceManager : IMediaSourceManager, IDisposable
{
+ // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+ private const char LiveStreamIdDelimeter = '_';
+
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
@@ -40,6 +43,9 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
+ private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
+ private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+
private IMediaSourceProvider[] _providers;
public MediaSourceManager(
@@ -368,7 +374,6 @@ namespace Emby.Server.Implementations.Library
}
}
-
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
@@ -451,9 +456,6 @@ namespace Emby.Server.Implementations.Library
.ToList();
}
- private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
- private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -619,12 +621,14 @@ namespace Emby.Server.Implementations.Library
if (liveStreamInfo is IDirectStreamProvider)
{
- var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = mediaSource,
- ExtractChapters = false,
- MediaType = DlnaProfileType.Video
- }, cancellationToken).ConfigureAwait(false);
+ var info = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ ExtractChapters = false,
+ MediaType = DlnaProfileType.Video
+ },
+ cancellationToken).ConfigureAwait(false);
mediaSource.MediaStreams = info.MediaStreams;
mediaSource.Container = info.Container;
@@ -855,24 +859,21 @@ namespace Emby.Server.Implementations.Library
}
}
- // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char LiveStreamIdDelimeter = '_';
-
- private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+ private (IMediaSourceProvider, string) GetProvider(string key)
{
if (string.IsNullOrEmpty(key))
{
- throw new ArgumentException("key");
+ throw new ArgumentException("Key can't be empty.", nameof(key));
}
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
- var splitIndex = key.IndexOf(LiveStreamIdDelimeter);
+ var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1);
- return new Tuple<IMediaSourceProvider, string>(provider, keyId);
+ return (provider, keyId);
}
/// <summary>
@@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
- private readonly object _disposeLock = new object();
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@@ -892,15 +893,12 @@ namespace Emby.Server.Implementations.Library
{
if (dispose)
{
- lock (_disposeLock)
+ foreach (var key in _openStreams.Keys.ToList())
{
- foreach (var key in _openStreams.Keys.ToList())
- {
- var task = CloseLiveStream(key);
-
- Task.WaitAll(task);
- }
+ CloseLiveStream(key).GetAwaiter().GetResult();
}
+
+ _liveStreamSemaphore.Dispose();
}
}
}
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index ca904c4ec..179e0ed98 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
}
// load forced subs if we have found no suitable full subtitles
- stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
+ stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
if (stream != null)
{
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 0bdc59914..877fdec86 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index c8e41001a..3332e1806 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -1,6 +1,5 @@
using System.Globalization;
using Emby.Naming.TV;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
@@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class SeasonResolver : FolderResolver<Season>
{
- private readonly IServerConfigurationManager _config;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
private readonly ILogger<SeasonResolver> _logger;
@@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
/// </summary>
- /// <param name="config">The config.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="localization">The localization.</param>
/// <param name="logger">The logger.</param>
public SeasonResolver(
- IServerConfigurationManager config,
ILibraryManager libraryManager,
ILocalizationManager localization,
ILogger<SeasonResolver> logger)
{
- _config = config;
_libraryManager = libraryManager;
_localization = localization;
_logger = logger;
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 3df9cc06f..9a69bce0e 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;
@@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
{
public class SearchEngine : ISearchEngine
{
- private readonly ILogger<SearchEngine> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
- public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
+ public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
{
- _logger = logger;
_libraryManager = libraryManager;
_userManager = userManager;
}
@@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{
User user = null;
-
- if (query.UserId.Equals(Guid.Empty))
- {
- }
- else
+ if (query.UserId != Guid.Empty)
{
user = _userManager.GetUserById(query.UserId);
}
@@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
if (query.StartIndex.HasValue)
{
- results = results.Skip(query.StartIndex.Value).ToList();
+ results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
}
if (query.Limit.HasValue)
{
- results = results.Take(query.Limit.Value).ToList();
+ results = results.GetRange(0, query.Limit.Value);
}
return new QueryResult<SearchHintInfo>
{
TotalRecordCount = totalRecordCount,
- Items = results.ToArray()
+ Items = results
};
}
@@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
if (string.IsNullOrEmpty(searchTerm))
{
- throw new ArgumentNullException("SearchTerm can't be empty.", nameof(searchTerm));
+ throw new ArgumentException("SearchTerm can't be empty.", nameof(query));
}
searchTerm = searchTerm.Trim().RemoveDiacritics();
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 175b3af57..f9e5e6bbc 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library
@@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
- private readonly ILogger<UserDataManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IUserManager _userManager;
private readonly IUserDataRepository _repository;
public UserDataManager(
- ILogger<UserDataManager> logger,
IServerConfigurationManager config,
IUserManager userManager,
IUserDataRepository repository)
{
- _logger = logger;
_config = config;
_userManager = userManager;
_repository = repository;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 7b0fcbc9e..80e09f0a3 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index d8ec107ec..612dc5238 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -230,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (filters.Count > 0)
{
- output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
+ output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray()));
}
return output;
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 3709f8fe4..77a7069eb 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
ListingsProviderInfo info,
List<string> programIds,
- CancellationToken cancellationToken)
+ CancellationToken cancellationToken)
{
if (programIds.Count == 0)
{
@@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var imageId = i.Substring(0, 10);
- if (!imageIdString.Contains(imageId))
+ if (!imageIdString.Contains(imageId, StringComparison.Ordinal))
{
imageIdString += "\"" + imageId + "\",";
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 0a93c4674..f33d07174 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
&& !programInfo.IsRepeat
&& (programInfo.EpisodeNumber ?? 0) == 0)
{
- programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
+ programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
}
}
else
@@ -246,7 +246,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
// Construct an id from the channel and start date
- programInfo.Id = string.Format("{0}_{1:O}", program.ChannelId, program.StartDate);
+ programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate);
if (programInfo.IsMovie)
{
@@ -296,7 +296,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Name = c.DisplayName,
ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null,
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 4c1de3bcc..90cbd85a5 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
@@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -41,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv
/// </summary>
public class LiveTvManager : ILiveTvManager, IDisposable
{
+ private const int MaxGuideDays = 14;
private const string ExternalServiceTag = "ExternalServiceId";
private const string EtagKey = "ProgramEtag";
@@ -54,7 +55,6 @@ namespace Emby.Server.Implementations.LiveTv
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
@@ -73,7 +73,6 @@ namespace Emby.Server.Implementations.LiveTv
ILibraryManager libraryManager,
ITaskManager taskManager,
ILocalizationManager localization,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IChannelManager channelManager,
LiveTvDtoService liveTvDtoService)
@@ -85,7 +84,6 @@ namespace Emby.Server.Implementations.LiveTv
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
- _jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_dtoService = dtoService;
_userDataManager = userDataManager;
@@ -563,7 +561,7 @@ namespace Emby.Server.Implementations.LiveTv
item.Audio = info.Audio;
item.ChannelId = channel.Id;
- item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
+ item.CommunityRating ??= info.CommunityRating;
if ((item.CommunityRating ?? 0).Equals(0))
{
item.CommunityRating = null;
@@ -648,8 +646,8 @@ namespace Emby.Server.Implementations.LiveTv
item.IsSeries = isSeries;
item.Name = info.Name;
- item.OfficialRating = item.OfficialRating ?? info.OfficialRating;
- item.Overview = item.Overview ?? info.Overview;
+ item.OfficialRating ??= info.OfficialRating;
+ item.Overview ??= info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
item.ProviderIds = info.ProviderIds;
@@ -686,19 +684,23 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.ImagePath))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.ImagePath,
- Type = ImageType.Primary
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ImagePath,
+ Type = ImageType.Primary
+ },
+ 0);
}
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.ImageUrl,
- Type = ImageType.Primary
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ImageUrl,
+ Type = ImageType.Primary
+ },
+ 0);
}
}
@@ -706,11 +708,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.ThumbImageUrl,
- Type = ImageType.Thumb
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ThumbImageUrl,
+ Type = ImageType.Thumb
+ },
+ 0);
}
}
@@ -718,11 +722,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.LogoImageUrl,
- Type = ImageType.Logo
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.LogoImageUrl,
+ Type = ImageType.Logo
+ },
+ 0);
}
}
@@ -730,11 +736,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.BackdropImageUrl,
- Type = ImageType.Backdrop
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.BackdropImageUrl,
+ Type = ImageType.Backdrop
+ },
+ 0);
}
}
@@ -789,7 +797,6 @@ namespace Emby.Server.Implementations.LiveTv
if (query.OrderBy.Count == 0)
{
-
// Unless something else was specified, order by start date to take advantage of a specialized index
query.OrderBy = new[]
{
@@ -827,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
{
- var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
+ var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false);
var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
if (seriesTimer != null)
{
@@ -850,13 +857,11 @@ namespace Emby.Server.Implementations.LiveTv
var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user);
- var result = new QueryResult<BaseItemDto>
+ return new QueryResult<BaseItemDto>
{
Items = returnArray,
TotalRecordCount = queryResult.TotalRecordCount
};
-
- return result;
}
public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
@@ -1176,7 +1181,6 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
-
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
@@ -1301,8 +1305,6 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- private const int MaxGuideDays = 14;
-
private double GetGuideDays()
{
var config = GetConfiguration();
@@ -1715,7 +1717,7 @@ namespace Emby.Server.Implementations.LiveTv
if (timer == null)
{
- throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id));
+ throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id));
}
var service = GetService(timer.ServiceName);
@@ -1734,7 +1736,7 @@ namespace Emby.Server.Implementations.LiveTv
if (timer == null)
{
- throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id));
+ throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id));
}
var service = GetService(timer.ServiceName);
@@ -1746,10 +1748,12 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
{
- var results = await GetTimers(new TimerQuery
- {
- Id = id
- }, cancellationToken).ConfigureAwait(false);
+ var results = await GetTimers(
+ new TimerQuery
+ {
+ Id = id
+ },
+ cancellationToken).ConfigureAwait(false);
return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
}
@@ -1797,10 +1801,7 @@ namespace Emby.Server.Implementations.LiveTv
}
var returnArray = timers
- .Select(i =>
- {
- return i.Item1;
- })
+ .Select(i => i.Item1)
.ToArray();
return new QueryResult<SeriesTimerInfo>
@@ -1971,7 +1972,7 @@ namespace Emby.Server.Implementations.LiveTv
if (service == null)
{
- service = _services.First();
+ service = _services[0];
}
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@@ -1997,9 +1998,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false);
- var obj = _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
-
- return obj;
+ return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
}
public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken)
@@ -2128,6 +2127,7 @@ namespace Emby.Server.Implementations.LiveTv
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
private bool _disposed = false;
@@ -2234,7 +2234,7 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
{
- info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info));
+ info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2278,7 +2278,7 @@ namespace Emby.Server.Implementations.LiveTv
{
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
- info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info));
+ info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2450,8 +2450,7 @@ namespace Emby.Server.Implementations.LiveTv
.SelectMany(i => i.Locations)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(i => _libraryManager.FindByPath(i, true))
- .Where(i => i != null)
- .Where(i => i.IsVisibleStandalone(user))
+ .Where(i => i != null && i.IsVisibleStandalone(user))
.SelectMany(i => _libraryManager.GetCollectionFolders(i))
.GroupBy(x => x.Id)
.Select(x => x.First())
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index f3fc41352..8a0c0043a 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv
public class LiveTvMediaSourceProvider : IMediaSourceProvider
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char StreamIdDelimeter = '_';
- private const string StreamIdDelimeterString = "_";
+ private const char StreamIdDelimiter = '_';
private readonly ILiveTvManager _liveTvManager;
private readonly ILogger<LiveTvMediaSourceProvider> _logger;
@@ -47,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
+ return Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
}
private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
@@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv
source.Id ?? string.Empty
};
- source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
+ source.OpenToken = string.Join(StreamIdDelimiter, openKeys);
}
// Dummy this up so that direct play checks can still run
@@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
/// <inheritdoc />
public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
- var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
+ var keys = openToken.Split(StreamIdDelimiter, 3);
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index a8d34d19c..fbcd4ef37 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -1,10 +1,10 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -14,7 +14,7 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
@@ -23,17 +23,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
protected readonly IServerConfigurationManager Config;
protected readonly ILogger<BaseTunerHost> Logger;
- protected IJsonSerializer JsonSerializer;
protected readonly IFileSystem FileSystem;
- private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
- new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
+ private readonly IMemoryCache _memoryCache;
- protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
+ protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
{
Config = config;
Logger = logger;
- JsonSerializer = jsonSerializer;
+ _memoryCache = memoryCache;
FileSystem = fileSystem;
}
@@ -44,23 +42,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{
- ChannelCache cache = null;
var key = tuner.Id;
- if (enableCache && !string.IsNullOrEmpty(key) && _channelCache.TryGetValue(key, out cache))
+ if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
{
- return cache.Channels.ToList();
+ return cache;
}
- var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
- var list = result.ToList();
+ var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
// logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
if (!string.IsNullOrEmpty(key) && list.Count > 0)
{
- cache = cache ?? new ChannelCache();
- cache.Channels = list;
- _channelCache.AddOrUpdate(key, cache, (k, v) => cache);
+ _memoryCache.Set(key, list);
}
return list;
@@ -95,7 +89,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
- JsonSerializer.SerializeToFile(channels, channelCacheFile);
+ await using var writeStream = File.OpenWrite(channelCacheFile);
+ await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (IOException)
{
@@ -110,7 +105,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- var channels = JsonSerializer.DeserializeFromFile<List<ChannelInfo>>(channelCacheFile);
+ await using var readStream = File.OpenRead(channelCacheFile);
+ var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
list.AddRange(channels);
}
catch (IOException)
@@ -233,10 +230,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
return Config.GetConfiguration<LiveTvOptions>("livetv");
}
-
- private class ChannelCache
- {
- public List<ChannelInfo> Channels;
- }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 2e2488e6e..2b5f69d41 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -23,7 +24,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -36,17 +37,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
+ private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
+
public HdHomerunHost(
IServerConfigurationManager config,
ILogger<HdHomerunHost> logger,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IHttpClient httpClient,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
INetworkManager networkManager,
- IStreamHelper streamHelper)
- : base(config, logger, jsonSerializer, fileSystem)
+ IStreamHelper streamHelper,
+ IMemoryCache memoryCache)
+ : base(config, logger, fileSystem, memoryCache)
{
_httpClient = httpClient;
_appHost = appHost;
@@ -75,18 +78,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
BufferContent = false
};
- using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
- using (var stream = response.Content)
- {
- var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
-
- if (info.ImportFavoritesOnly)
- {
- lineup = lineup.Where(i => i.Favorite).ToList();
- }
+ using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+ await using var stream = response.Content;
+ var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
+ .ConfigureAwait(false) ?? new List<Channels>();
- return lineup.Where(i => !i.DRM).ToList();
+ if (info.ImportFavoritesOnly)
+ {
+ lineup = lineup.Where(i => i.Favorite).ToList();
}
+
+ return lineup.Where(i => !i.DRM).ToList();
}
private class HdHomerunChannelInfo : ChannelInfo
@@ -114,7 +116,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}).Cast<ChannelInfo>().ToList();
}
- private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
{
var cacheKey = info.Id;
@@ -132,35 +133,35 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try
{
- using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
+ using var response = await _httpClient.SendAsync(
+ new HttpRequestOptions
{
- Url = string.Format("{0}/discover.json", GetApiUrl(info)),
+ Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)),
CancellationToken = cancellationToken,
BufferContent = false
- }, HttpMethod.Get).ConfigureAwait(false))
- using (var stream = response.Content)
- {
- var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
+ }, HttpMethod.Get).ConfigureAwait(false);
+ await using var stream = response.Content;
+ var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
- if (!string.IsNullOrEmpty(cacheKey))
+ if (!string.IsNullOrEmpty(cacheKey))
+ {
+ lock (_modelCache)
{
- lock (_modelCache)
- {
- _modelCache[cacheKey] = discoverResponse;
- }
+ _modelCache[cacheKey] = discoverResponse;
}
-
- return discoverResponse;
}
+
+ return discoverResponse;
}
catch (HttpException ex)
{
- if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
+ if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
{
- var defaultValue = "HDHR";
+ const string DefaultValue = "HDHR";
var response = new DiscoverResponse
{
- ModelNumber = defaultValue
+ ModelNumber = DefaultValue
};
if (!string.IsNullOrEmpty(cacheKey))
{
@@ -182,12 +183,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
- {
- Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
- CancellationToken = cancellationToken,
- BufferContent = false
- }, HttpMethod.Get).ConfigureAwait(false))
+ using (var response = await _httpClient.SendAsync(
+ new HttpRequestOptions()
+ {
+ Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)),
+ CancellationToken = cancellationToken,
+ BufferContent = false
+ },
+ HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
{
@@ -195,7 +198,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
while (!sr.EndOfStream)
{
string line = StripXML(sr.ReadLine());
- if (line.Contains("Channel"))
+ if (line.Contains("Channel", StringComparison.Ordinal))
{
LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
@@ -226,6 +229,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static string StripXML(string source)
{
+ if (string.IsNullOrEmpty(source))
+ {
+ return string.Empty;
+ }
+
char[] buffer = new char[source.Length];
int bufferIndex = 0;
bool inside = false;
@@ -270,7 +278,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
for (int i = 0; i < model.TunerCount; ++i)
{
- var name = string.Format("Tuner {0}", i + 1);
+ var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
var currentChannel = "none"; // @todo Get current channel and map back to Station Id
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
@@ -725,7 +733,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Need a way to set the Receive timeout on the socket otherwise this might never timeout?
try
{
- await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken);
+ await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false);
var receiveBuffer = new byte[8192];
while (!cancellationToken.IsCancellationRequested)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 57c5b7500..d4a88e299 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- public class HdHomerunManager : IDisposable
+ public sealed class HdHomerunManager : IDisposable
{
public const int HdHomeRunPort = 65001;
@@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
+
+ GC.SuppressFinalize(this);
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
@@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
_activeTuner = i;
- var lockKeyString = string.Format("{0:d}", lockKeyValue);
+ var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
@@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
continue;
}
- var commandList = commands.GetCommands();
- foreach (var command in commandList)
+ foreach (var command in commands.GetCommands())
{
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
@@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
+ var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index ff42a9747..8fc29fb4a 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -18,7 +18,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -36,13 +36,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IServerConfigurationManager config,
IMediaSourceManager mediaSourceManager,
ILogger<M3UTunerHost> logger,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IHttpClient httpClient,
IServerApplicationHost appHost,
INetworkManager networkManager,
- IStreamHelper streamHelper)
- : base(config, logger, jsonSerializer, fileSystem)
+ IStreamHelper streamHelper,
+ IMemoryCache memoryCache)
+ : base(config, logger, fileSystem, memoryCache)
{
_httpClient = httpClient;
_appHost = appHost;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index c798c0a85..875977219 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
{
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null;
+ var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
string numberString = null;
string attributeValue;
- double doubleValue;
if (attributes.TryGetValue("tvg-chno", out attributeValue))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+ if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;
}
@@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
if (attributes.TryGetValue("tvg-id", out attributeValue))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+ if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;
}
else if (attributes.TryGetValue("channel-id", out attributeValue))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+ if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;
}
}
}
- if (String.IsNullOrWhiteSpace(numberString))
+ if (string.IsNullOrWhiteSpace(numberString))
{
// Using this as a fallback now as this leads to Problems with channels like "5 USA"
// where 5 isnt ment to be the channel number
// Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz
- if (!string.IsNullOrWhiteSpace(nameInExtInf))
+ if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace())
{
var numberIndex = nameInExtInf.IndexOf(' ');
if (numberIndex > 0)
{
- var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
+ var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
- if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
+ if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
- numberString = numberPart;
+ numberString = numberPart.ToString();
}
}
}
@@ -231,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
+ numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
if (!IsValidChannelNumber(numberString))
{
@@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return false;
}
- if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
+ if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
return false;
}
@@ -281,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 var number))
+ if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
// channel.Number = number.ToString();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 20447347b..e587c37d5 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -19,8 +19,8 @@
"Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies",
- "DeviceOnlineWithName": "{0} is verbind",
- "DeviceOfflineWithName": "{0} het afgesluit",
+ "DeviceOnlineWithName": "{0} gekoppel is",
+ "DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
"HeaderLiveTV": "Live TV",
@@ -91,5 +91,9 @@
"ChapterNameValue": "Hoofstuk",
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
- "Albums": "Albums"
+ "Albums": "Albums",
+ "TasksChannelsCategory": "Internet kanale",
+ "TasksApplicationCategory": "aansoek",
+ "TasksLibraryCategory": "biblioteek",
+ "TasksMaintenanceCategory": "onderhoud"
}
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 1f309f3ff..1bd190982 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -1,26 +1,26 @@
{
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
- "Collections": "সংকলন",
+ "Collections": "কলেক্শন",
"ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল",
- "CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
+ "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "বই",
- "AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
- "Artists": "শিল্পী",
+ "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
+ "Artists": "শিল্পীরা",
"Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামগুলো",
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
- "HeaderCameraUploads": "ক্যামেরার আপলোডগুলো",
- "HeaderAlbumArtists": "এলবামের শিল্পী",
- "Genres": "ঘরানা",
+ "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
+ "HeaderAlbumArtists": "এলবাম শিল্পী",
+ "Genres": "জেনার",
"Folders": "ফোল্ডারগুলো",
- "Favorites": "ফেভারিটগুলো",
- "FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ",
- "AppDeviceValues": "এপ: {0}, ডিভাইস: {0}",
+ "Favorites": "পছন্দসমূহ",
+ "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
+ "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
@@ -74,20 +74,20 @@
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
"MusicVideos": "গানের ভিডিও",
"Music": "গান",
- "Movies": "সিনেমা",
+ "Movies": "চলচ্চিত্র",
"MixedContent": "মিশ্র কন্টেন্ট",
- "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে",
- "HeaderRecordingGroups": "রেকর্ডিং গ্রুপ",
- "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে",
- "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে",
- "MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে",
- "Latest": "একদম নতুন",
+ "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
+ "HeaderRecordingGroups": "রেকর্ডিং দল",
+ "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
+ "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
+ "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
+ "Latest": "সর্বশেষ",
"LabelRunningTimeValue": "চলার সময়: {0}",
- "LabelIpAddressValue": "আইপি ঠিকানা: {0}",
+ "LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "থেকে পাওয়া",
- "HomeVideos": "বাসার ভিডিও",
+ "HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 82df43be1..fcbe9566e 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
- "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
+ "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
"Books": "Bücher",
- "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
+ "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
"Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen",
@@ -101,12 +101,12 @@
"TaskCleanTranscode": "Lösche Transkodier Pfad",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
"TaskUpdatePlugins": "Update Plugins",
- "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
- "TaskRefreshPeople": "Erneuere Schausteller",
+ "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+ "TaskRefreshPeople": "Erneuere Schauspieler",
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
"TaskCleanLogs": "Lösche Log Pfad",
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
- "TaskRefreshLibrary": "Scanne alle Bibliotheken",
+ "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
"TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
"TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 682f5325b..dc3a98154 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -18,13 +18,13 @@
"HeaderAlbumArtists": "אמני האלבום",
"HeaderCameraUploads": "העלאות ממצלמה",
"HeaderContinueWatching": "המשך לצפות",
- "HeaderFavoriteAlbums": "אלבומים שאהבתי",
+ "HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים",
"HeaderFavoriteEpisodes": "פרקים מועדפים",
- "HeaderFavoriteShows": "סדרות מועדפות",
+ "HeaderFavoriteShows": "תוכניות מועדפות",
"HeaderFavoriteSongs": "שירים מועדפים",
"HeaderLiveTV": "שידורים חיים",
- "HeaderNextUp": "הבא",
+ "HeaderNextUp": "הבא בתור",
"HeaderRecordingGroups": "קבוצות הקלטה",
"HomeVideos": "סרטונים בייתים",
"Inherit": "הורש",
@@ -45,37 +45,37 @@
"NameSeasonNumber": "עונה {0}",
"NameSeasonUnknown": "עונה לא ידועה",
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
- "NotificationOptionApplicationUpdateAvailable": "Application update available",
- "NotificationOptionApplicationUpdateInstalled": "Application update installed",
- "NotificationOptionAudioPlayback": "Audio playback started",
- "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
- "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+ "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
+ "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
+ "NotificationOptionAudioPlayback": "ניגון שמע החל",
+ "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
+ "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
"NotificationOptionInstallationFailed": "התקנה נכשלה",
- "NotificationOptionNewLibraryContent": "New content added",
- "NotificationOptionPluginError": "Plugin failure",
+ "NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
+ "NotificationOptionPluginError": "כשלון בתוסף",
"NotificationOptionPluginInstalled": "התוסף הותקן",
"NotificationOptionPluginUninstalled": "התוסף הוסר",
"NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
"NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
- "NotificationOptionTaskFailed": "Scheduled task failure",
- "NotificationOptionUserLockedOut": "User locked out",
- "NotificationOptionVideoPlayback": "Video playback started",
- "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
+ "NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
+ "NotificationOptionUserLockedOut": "משתמש ננעל",
+ "NotificationOptionVideoPlayback": "ניגון וידאו החל",
+ "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
"Photos": "תמונות",
"Playlists": "רשימות הפעלה",
"Plugin": "Plugin",
- "PluginInstalledWithName": "{0} was installed",
- "PluginUninstalledWithName": "{0} was uninstalled",
- "PluginUpdatedWithName": "{0} was updated",
+ "PluginInstalledWithName": "{0} הותקן",
+ "PluginUninstalledWithName": "{0} הוסר",
+ "PluginUpdatedWithName": "{0} עודכן",
"ProviderValue": "Provider: {0}",
- "ScheduledTaskFailedWithName": "{0} failed",
- "ScheduledTaskStartedWithName": "{0} started",
- "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+ "ScheduledTaskFailedWithName": "{0} נכשל",
+ "ScheduledTaskStartedWithName": "{0} החל",
+ "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות",
"Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+ "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
"Sync": "סנכרן",
"System": "System",
"TvShows": "סדרות טלוויזיה",
@@ -83,14 +83,14 @@
"UserCreatedWithName": "המשתמש {0} נוצר",
"UserDeletedWithName": "המשתמש {0} הוסר",
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
- "UserLockedOutWithName": "User {0} has been locked out",
- "UserOfflineFromDevice": "{0} has disconnected from {1}",
- "UserOnlineFromDevice": "{0} is online from {1}",
- "UserPasswordChangedWithName": "Password has been changed for user {0}",
- "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+ "UserLockedOutWithName": "המשתמש {0} ננעל",
+ "UserOfflineFromDevice": "{0} התנתק מ-{1}",
+ "UserOnlineFromDevice": "{0} מחובר מ-{1}",
+ "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
+ "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
- "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+ "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
"ValueSpecialEpisodeName": "מיוחד- {0}",
"VersionNumber": "Version {0}",
"TaskRefreshLibrary": "סרוק ספריית מדיה",
@@ -109,7 +109,7 @@
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט",
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
- "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
+ "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
"TaskRefreshChannels": "רענן ערוץ",
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index eabdb9138..ccb72ff93 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -7,8 +7,8 @@
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru",
"LabelIpAddressValue": "Alamat IP: {0}",
- "ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan",
- "ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan",
+ "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
+ "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
"Inherit": "Warisan",
"HomeVideos": "Video Rumah",
"HeaderRecordingGroups": "Grup Rekaman",
@@ -19,8 +19,8 @@
"HeaderFavoriteEpisodes": "Episode Favorit",
"HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit",
- "HeaderContinueWatching": "Masih Melihat",
- "HeaderCameraUploads": "Uplod Kamera",
+ "HeaderContinueWatching": "Lanjutkan Menonton",
+ "HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis",
"Genres": "Genre",
"Folders": "Folder",
@@ -32,11 +32,11 @@
"ChapterNameValue": "Bagian {0}",
"Channels": "Saluran",
"TvShows": "Seri TV",
- "SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}",
- "StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.",
+ "SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
+ "StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
"Songs": "Lagu",
"Playlists": "Daftar putar",
- "NotificationOptionPluginUninstalled": "Plugin dilepas",
+ "NotificationOptionPluginUninstalled": "Plugin dihapus",
"MusicVideos": "Video musik",
"VersionNumber": "Versi {0}",
"ValueSpecialEpisodeName": "Spesial - {0}",
@@ -65,7 +65,7 @@
"Photos": "Foto",
"NotificationOptionUserLockedOut": "Pengguna terkunci",
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
- "NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan",
+ "NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
"NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
"NotificationOptionPluginInstalled": "Plugin terpasang",
"NotificationOptionPluginError": "Kegagalan plugin",
@@ -74,14 +74,14 @@
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
- "NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.",
+ "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
"NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}",
- "NameInstallFailed": "{0} instalasi gagal",
+ "NameInstallFailed": "{0} penginstalan gagal",
"Music": "Musik",
"Movies": "Film",
- "MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui",
- "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui",
+ "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
@@ -90,6 +90,28 @@
"NotificationOptionVideoPlayback": "Pemutaran video dimulai",
"NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
"NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
- "MixedContent": "Konten campur",
- "PluginUninstalledWithName": "{0} telah dihapus"
+ "MixedContent": "Konten campuran",
+ "PluginUninstalledWithName": "{0} telah dihapus",
+ "TaskRefreshChapterImagesDescription": "Membuat gambar mini untuk video yang memiliki bagian.",
+ "TaskRefreshChapterImages": "Ekstrak Gambar Bagian",
+ "TaskCleanCacheDescription": "Menghapus file cache yang tidak lagi dibutuhkan oleh sistem.",
+ "TaskCleanCache": "Bersihkan Cache Direktori",
+ "TasksLibraryCategory": "Pustaka",
+ "TasksMaintenanceCategory": "Perbaikan",
+ "TasksApplicationCategory": "Aplikasi",
+ "TaskRefreshPeopleDescription": "Memperbarui metadata untuk aktor dan sutradara di pustaka media Anda.",
+ "TaskRefreshLibraryDescription": "Memindai Pustaka media Anda untuk mencari file baru dan memperbarui metadata.",
+ "TasksChannelsCategory": "Saluran Online",
+ "TaskDownloadMissingSubtitlesDescription": "Mencari di internet untuk subtitle yang hilang berdasarkan konfigurasi metadata.",
+ "TaskDownloadMissingSubtitles": "Unduh subtitle yang hilang",
+ "TaskRefreshChannelsDescription": "Segarkan informasi saluran internet.",
+ "TaskRefreshChannels": "Segarkan Saluran",
+ "TaskCleanTranscodeDescription": "Menghapus file transcode yang berumur lebih dari satu hari.",
+ "TaskCleanTranscode": "Bersihkan Direktori Transcode",
+ "TaskUpdatePluginsDescription": "Unduh dan instal pembaruan untuk plugin yang dikonfigurasi untuk memperbarui secara otomatis.",
+ "TaskUpdatePlugins": "Perbarui Plugin",
+ "TaskRefreshPeople": "Muat ulang Orang",
+ "TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.",
+ "TaskCleanLogs": "Bersihkan Log Direktori",
+ "TaskRefreshLibrary": "Pindai Pustaka Media"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 7f5a56e86..bf1a0ef13 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -84,8 +84,8 @@
"UserDeletedWithName": "L'utente {0} è stato rimosso",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
- "UserOfflineFromDevice": "{0} è stato disconnesso da {1}",
- "UserOnlineFromDevice": "{0} è online da {1}",
+ "UserOfflineFromDevice": "{0} si è disconnesso su {1}",
+ "UserOnlineFromDevice": "{0} è online su {1}",
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
"UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
@@ -102,11 +102,11 @@
"TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
"TaskUpdatePlugins": "Aggiorna i Plugin",
"TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
- "TaskRefreshPeople": "Aggiorna persone",
+ "TaskRefreshPeople": "Aggiornamento Persone",
"TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
"TaskCleanLogs": "Pulisci la cartella dei log",
"TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
- "TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali",
+ "TaskRefreshLibrary": "Scan Librerie",
"TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
"TaskRefreshChapterImages": "Estrai immagini capitolo",
"TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index 50b6360d8..b6db2b0f2 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -57,5 +57,7 @@
"HeaderCameraUploads": "कॅमेरा अपलोड",
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
"Application": "अ‍ॅप्लिकेशन",
- "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}"
+ "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
+ "Collections": "संग्रह",
+ "ChapterNameValue": "धडा {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 79d078d4a..7f8df1289 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -5,47 +5,47 @@
"Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku",
- "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+ "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
"Channels": "Saluran",
- "ChapterNameValue": "Chapter {0}",
+ "ChapterNameValue": "Bab {0}",
"Collections": "Koleksi",
- "DeviceOfflineWithName": "{0} has disconnected",
- "DeviceOnlineWithName": "{0} is connected",
+ "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
+ "DeviceOnlineWithName": "{0} telah disambung",
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
- "Favorites": "Favorites",
- "Folders": "Folders",
+ "Favorites": "Kegemaran",
+ "Folders": "Fail-fail",
"Genres": "Genre-genre",
- "HeaderAlbumArtists": "Album Artists",
+ "HeaderAlbumArtists": "Album Artis-artis",
"HeaderCameraUploads": "Muatnaik Kamera",
"HeaderContinueWatching": "Terus Menonton",
- "HeaderFavoriteAlbums": "Favorite Albums",
- "HeaderFavoriteArtists": "Favorite Artists",
- "HeaderFavoriteEpisodes": "Favorite Episodes",
- "HeaderFavoriteShows": "Favorite Shows",
- "HeaderFavoriteSongs": "Favorite Songs",
- "HeaderLiveTV": "Live TV",
- "HeaderNextUp": "Next Up",
- "HeaderRecordingGroups": "Recording Groups",
- "HomeVideos": "Home videos",
- "Inherit": "Inherit",
- "ItemAddedWithName": "{0} was added to the library",
- "ItemRemovedWithName": "{0} was removed from the library",
+ "HeaderFavoriteAlbums": "Album-album Kegemaran",
+ "HeaderFavoriteArtists": "Artis-artis Kegemaran",
+ "HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
+ "HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
+ "HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
+ "HeaderLiveTV": "TV Siaran Langsung",
+ "HeaderNextUp": "Seterusnya",
+ "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
+ "HomeVideos": "Video Personal",
+ "Inherit": "Mewarisi",
+ "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
+ "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
"LabelIpAddressValue": "Alamat IP: {0}",
- "LabelRunningTimeValue": "Running time: {0}",
- "Latest": "Latest",
- "MessageApplicationUpdated": "Jellyfin Server has been updated",
- "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
- "MessageServerConfigurationUpdated": "Server configuration has been updated",
- "MixedContent": "Mixed content",
- "Movies": "Movies",
+ "LabelRunningTimeValue": "Masa berjalan: {0}",
+ "Latest": "Terbaru",
+ "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
+ "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
+ "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
+ "MixedContent": "Kandungan campuran",
+ "Movies": "Filem",
"Music": "Muzik",
"MusicVideos": "Video muzik",
- "NameInstallFailed": "{0} installation failed",
- "NameSeasonNumber": "Season {0}",
- "NameSeasonUnknown": "Season Unknown",
- "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
- "NotificationOptionApplicationUpdateAvailable": "Application update available",
+ "NameInstallFailed": "{0} pemasangan gagal",
+ "NameSeasonNumber": "Musim {0}",
+ "NameSeasonUnknown": "Musim Tidak Diketahui",
+ "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
+ "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
"NotificationOptionAudioPlayback": "Audio playback started",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 5365fff23..b534d0bbe 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -104,5 +104,14 @@
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
"TasksChannelsCategory": "Canais de Internet",
- "TaskRefreshChapterImages": "Extrair Imagens do Capítulo"
+ "TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
+ "TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
+ "TaskDownloadMissingSubtitles": "Download das legendas em falta",
+ "TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
+ "TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
+ "TaskCleanTranscode": "Limpar o diretório de Transcode",
+ "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
+ "TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
+ "TaskRefreshPeople": "Atualizar pessoas",
+ "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 71ee6446c..648aa384b 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -21,7 +21,7 @@
"HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители",
"HeaderFavoriteEpisodes": "Избранные эпизоды",
- "HeaderFavoriteShows": "Избранные передачи",
+ "HeaderFavoriteShows": "Избранные сериалы",
"HeaderFavoriteSongs": "Избранные композиции",
"HeaderLiveTV": "Эфир",
"HeaderNextUp": "Очередное",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index f722dd8c0..d6be86da3 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -45,7 +45,7 @@
"TvShows": "தொலைக்காட்சித் தொடர்கள்",
"Sync": "ஒத்திசைவு",
"StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
- "Songs": "பாட்டுகள்",
+ "Songs": "பாடல்கள்",
"Shows": "தொடர்கள்",
"ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
"ScheduledTaskStartedWithName": "{0} துவங்கியது",
@@ -93,7 +93,25 @@
"Channels": "சேனல்கள்",
"Books": "புத்தகங்கள்",
"AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
- "Artists": "கலைஞர்கள்",
+ "Artists": "கலைஞர்",
"Application": "செயலி",
- "Albums": "ஆல்பங்கள்"
+ "Albums": "ஆல்பங்கள்",
+ "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
+ "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
+ "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
+ "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
+ "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
+ "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+ "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
+ "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+ "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
+ "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
+ "TaskCleanLogs": "பதிவு அடைவு சுத்தம் செய்யுங்கள்",
+ "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
+ "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
+ "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
+ "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
+ "HomeVideos": "முகப்பு வீடியோக்கள்",
+ "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
+ "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
}
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 32538ac03..576aaeb1b 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -67,5 +67,7 @@
"Artists": "นักแสดง",
"Application": "แอปพลิเคชั่น",
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
- "Albums": "อัลบั้ม"
+ "Albums": "อัลบั้ม",
+ "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
+ "ScheduledTaskFailedWithName": "{0} ล้มเหลว"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index b2e0b66fe..e673465a4 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -1,13 +1,13 @@
{
- "MusicVideos": "Музичні відео",
+ "MusicVideos": "Музичні кліпи",
"Music": "Музика",
"Movies": "Фільми",
- "MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
- "MessageApplicationUpdated": "Jellyfin Server був оновлений",
+ "MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
+ "MessageApplicationUpdated": "Jellyfin Server оновлено",
"Latest": "Останні",
- "LabelIpAddressValue": "IP-адреси: {0}",
- "ItemRemovedWithName": "{0} видалено з бібліотеки",
- "ItemAddedWithName": "{0} додано до бібліотеки",
+ "LabelIpAddressValue": "IP-адреса: {0}",
+ "ItemRemovedWithName": "{0} видалено з медіатеки",
+ "ItemAddedWithName": "{0} додано до медіатеки",
"HeaderNextUp": "Наступний",
"HeaderLiveTV": "Ефірне ТБ",
"HeaderFavoriteSongs": "Улюблені пісні",
@@ -17,20 +17,101 @@
"HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд",
"HeaderCameraUploads": "Завантажено з камери",
- "HeaderAlbumArtists": "Виконавці альбомів",
+ "HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри",
- "Folders": "Директорії",
+ "Folders": "Каталоги",
"Favorites": "Улюблені",
- "DeviceOnlineWithName": "{0} під'єднано",
- "DeviceOfflineWithName": "{0} від'єднано",
+ "DeviceOnlineWithName": "Пристрій {0} підключився",
+ "DeviceOfflineWithName": "Пристрій {0} відключився",
"Collections": "Колекції",
- "ChapterNameValue": "Глава {0}",
+ "ChapterNameValue": "Розділ {0}",
"Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
"Books": "Книги",
- "AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
+ "AuthenticationSucceededWithUserName": "{0} успішно авторизований",
"Artists": "Виконавці",
"Application": "Додаток",
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
- "Albums": "Альбоми"
+ "Albums": "Альбоми",
+ "NotificationOptionServerRestartRequired": "Необхідно перезапустити сервер",
+ "NotificationOptionPluginUpdateInstalled": "Встановлено оновлення плагіна",
+ "NotificationOptionPluginUninstalled": "Плагін видалено",
+ "NotificationOptionPluginInstalled": "Плагін встановлено",
+ "NotificationOptionPluginError": "Помилка плагіна",
+ "NotificationOptionNewLibraryContent": "Додано новий контент",
+ "HomeVideos": "Домашнє відео",
+ "FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
+ "LabelRunningTimeValue": "Тривалість: {0}",
+ "TaskDownloadMissingSubtitlesDescription": "Шукає в Інтернеті відсутні субтитри на основі конфігурації метаданих.",
+ "TaskDownloadMissingSubtitles": "Завантажити відсутні субтитри",
+ "TaskRefreshChannelsDescription": "Оновлення інформації про Інтернет-канали.",
+ "TaskRefreshChannels": "Оновити канали",
+ "TaskCleanTranscodeDescription": "Вилучає файли для перекодування старше одного дня.",
+ "TaskCleanTranscode": "Очистити каталог перекодування",
+ "TaskUpdatePluginsDescription": "Завантажує та встановлює оновлення для плагінів, налаштованих на автоматичне оновлення.",
+ "TaskUpdatePlugins": "Оновити плагіни",
+ "TaskRefreshPeopleDescription": "Оновлення метаданих для акторів та режисерів у вашій медіатеці.",
+ "TaskRefreshPeople": "Оновити людей",
+ "TaskCleanLogsDescription": "Видаляє файли журналу, яким більше {0} днів.",
+ "TaskCleanLogs": "Очистити журнали",
+ "TaskRefreshLibraryDescription": "Сканує медіатеку на нові файли та оновлює метадані.",
+ "TaskRefreshLibrary": "Сканувати медіатеку",
+ "TaskRefreshChapterImagesDescription": "Створює ескізи для відео, які мають розділи.",
+ "TaskRefreshChapterImages": "Створити ескізи розділів",
+ "TaskCleanCacheDescription": "Видаляє файли кешу, які більше не потрібні системі.",
+ "TaskCleanCache": "Очистити кеш",
+ "TasksChannelsCategory": "Інтернет-канали",
+ "TasksApplicationCategory": "Додаток",
+ "TasksLibraryCategory": "Медіатека",
+ "TasksMaintenanceCategory": "Обслуговування",
+ "VersionNumber": "Версія {0}",
+ "ValueSpecialEpisodeName": "Спецепізод - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} додано до медіатеки",
+ "UserStoppedPlayingItemWithValues": "{0} закінчив відтворення {1} на {2}",
+ "UserStartedPlayingItemWithValues": "{0} відтворює {1} на {2}",
+ "UserPolicyUpdatedWithName": "Політика користувача оновлена для {0}",
+ "UserPasswordChangedWithName": "Пароль змінено для користувача {0}",
+ "UserOnlineFromDevice": "{0} підключився з {1}",
+ "UserOfflineFromDevice": "{0} відключився від {1}",
+ "UserLockedOutWithName": "Користувача {0} заблоковано",
+ "UserDownloadingItemWithValues": "{0} завантажує {1}",
+ "UserDeletedWithName": "Користувача {0} видалено",
+ "UserCreatedWithName": "Користувача {0} створено",
+ "User": "Користувач",
+ "TvShows": "ТВ-шоу",
+ "System": "Система",
+ "Sync": "Синхронізація",
+ "SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
+ "StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
+ "Songs": "Пісні",
+ "Shows": "Шоу",
+ "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
+ "ScheduledTaskStartedWithName": "{0} розпочато",
+ "ScheduledTaskFailedWithName": "Помилка {0}",
+ "ProviderValue": "Постачальник: {0}",
+ "PluginUpdatedWithName": "{0} оновлено",
+ "PluginUninstalledWithName": "{0} видалено",
+ "PluginInstalledWithName": "{0} встановлено",
+ "Plugin": "Плагін",
+ "Playlists": "Плейлисти",
+ "Photos": "Фотографії",
+ "NotificationOptionVideoPlaybackStopped": "Відтворення відео зупинено",
+ "NotificationOptionVideoPlayback": "Розпочато відтворення відео",
+ "NotificationOptionUserLockedOut": "Користувача заблоковано",
+ "NotificationOptionTaskFailed": "Помилка запланованого завдання",
+ "NotificationOptionInstallationFailed": "Помилка встановлення",
+ "NotificationOptionCameraImageUploaded": "Фотографію завантажено",
+ "NotificationOptionAudioPlaybackStopped": "Відтворення аудіо зупинено",
+ "NotificationOptionAudioPlayback": "Розпочато відтворення аудіо",
+ "NotificationOptionApplicationUpdateInstalled": "Встановлено оновлення додатка",
+ "NotificationOptionApplicationUpdateAvailable": "Доступне оновлення додатка",
+ "NewVersionIsAvailable": "Для завантаження доступна нова версія Jellyfin Server.",
+ "NameSeasonUnknown": "Сезон Невідомий",
+ "NameSeasonNumber": "Сезон {0}",
+ "NameInstallFailed": "Не вдалося встановити {0}",
+ "MixedContent": "Змішаний контент",
+ "MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
+ "Inherit": "Успадкувати",
+ "HeaderRecordingGroups": "Групи запису"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index a22f66df9..a21cdad95 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -92,7 +92,7 @@
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
- "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
+ "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新插件",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 62a23118f..90e2766b8 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization
}
// Try splitting by : to handle "Germany: FSK 18"
- var index = rating.IndexOf(':');
+ var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
{
rating = rating.Substring(index).TrimStart(':').Trim();
@@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(culture));
}
- const string prefix = "Core";
- var key = prefix + culture;
+ const string Prefix = "Core";
+ var key = Prefix + culture;
return _dictionaries.GetOrAdd(
key,
- f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+ f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
}
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index b51c03446..4e25768cf 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
public sealed class UdpSocket : ISocket, IDisposable
{
private Socket _socket;
- private int _localPort;
+ private readonly int _localPort;
private bool _disposed = false;
public Socket Socket => _socket;
- public IPAddress LocalIPAddress { get; }
-
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
{
SocketFlags = SocketFlags.None
@@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
InitReceiveSocketAsyncEventArgs();
}
+ public UdpSocket(Socket socket, IPEndPoint endPoint)
+ {
+ if (socket == null)
+ {
+ throw new ArgumentNullException(nameof(socket));
+ }
+
+ _socket = socket;
+ _socket.Connect(endPoint);
+
+ InitReceiveSocketAsyncEventArgs();
+ }
+
+ public IPAddress LocalIPAddress { get; }
+
private void InitReceiveSocketAsyncEventArgs()
{
var receiveBuffer = new byte[8192];
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
- _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
+ _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
var sendBuffer = new byte[8192];
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
- _sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
+ _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
}
- private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+ private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
{
var tcs = _currentReceiveTaskCompletionSource;
if (tcs != null)
@@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
}
}
- private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+ private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
{
var tcs = _currentSendTaskCompletionSource;
if (tcs != null)
@@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
}
}
- public UdpSocket(Socket socket, IPEndPoint endPoint)
- {
- if (socket == null)
- {
- throw new ArgumentNullException(nameof(socket));
- }
-
- _socket = socket;
- _socket.Connect(endPoint);
-
- InitReceiveSocketAsyncEventArgs();
- }
-
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
{
ThrowIfDisposed();
@@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
}
}
+ /// <inheritdoc />
public void Dispose()
{
if (_disposed)
@@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
}
_socket?.Dispose();
+ _receiveSocketAsyncEventArgs.Dispose();
+ _sendSocketAsyncEventArgs.Dispose();
_currentReceiveTaskCompletionSource?.TrySetCanceled();
_currentSendTaskCompletionSource?.TrySetCanceled();
diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs
index 6aa1dfbc9..089ec30e6 100644
--- a/Emby.Server.Implementations/Networking/NetworkManager.cs
+++ b/Emby.Server.Implementations/Networking/NetworkManager.cs
@@ -152,7 +152,12 @@ namespace Emby.Server.Implementations.Networking
return true;
}
- byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes();
+ if (!IPAddress.TryParse(endpoint, out var ipAddress))
+ {
+ return false;
+ }
+
+ byte[] octet = ipAddress.GetAddressBytes();
if ((octet[0] == 10) ||
(octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
@@ -160,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
(octet[0] == 127) || // RFC1122
(octet[0] == 169 && octet[1] == 254)) // RFC3927
{
- return false;
+ return true;
}
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
@@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking
string excludeAddress = "[" + addressString + "]";
var subnets = LocalSubnetsFn();
+ // Include any address if LAN subnets aren't specified
+ if (subnets.Length == 0)
+ {
+ return true;
+ }
+
// Exclude any addresses if they appear in the LAN list in [ ]
if (Array.IndexOf(subnets, excludeAddress) != -1)
{
@@ -379,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
var host = uri.DnsSafeHost;
_logger.LogDebug("Resolving host {0}", host);
- address = GetIpAddresses(host).Result.FirstOrDefault();
+ address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
if (address != null)
{
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 5dd1af4b8..38ceadedb 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
- var hasArtist = child as IHasArtist;
- if (hasArtist != null)
+ if (child is IHasArtist hasArtist)
{
- entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+ entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
- var hasArtist = child as IHasArtist;
- if (hasArtist != null)
+ if (child is IHasArtist hasArtist)
{
- entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+ entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
{
- var playlist = new M3uPlaylist();
- playlist.IsExtended = true;
+ var playlist = new M3uPlaylist
+ {
+ IsExtended = true
+ };
foreach (var child in item.GetLinkedChildren())
{
var entry = new M3uPlaylistEntry()
@@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
{
- folderPath = folderPath + Path.DirectorySeparatorChar;
+ folderPath += Path.DirectorySeparatorChar;
}
var folderUri = new Uri(folderPath);
@@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
return relativePath;
}
- private static string UnEscape(string content)
- {
- if (content == null)
- {
- return content;
- }
-
- return content.Replace("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<");
- }
-
- private static string Escape(string content)
- {
- if (content == null)
- {
- return null;
- }
-
- return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;");
- }
-
public Folder GetPlaylistsFolder(Guid userId)
{
- var typeName = "PlaylistsFolder";
+ const string TypeName = "PlaylistsFolder";
- return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
- _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
+ return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
+ _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 3fe15ec68..81096026b 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly IJsonSerializer _jsonSerializer;
private readonly IApplicationPaths _applicationPaths;
private readonly ILogger<TaskManager> _logger;
- private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="TaskManager" /> class.
@@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="applicationPaths">The application paths.</param>
/// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The filesystem manager.</param>
public TaskManager(
IApplicationPaths applicationPaths,
IJsonSerializer jsonSerializer,
- ILogger<TaskManager> logger,
- IFileSystem fileSystem)
+ ILogger<TaskManager> logger)
{
_applicationPaths = applicationPaths;
_jsonSerializer = jsonSerializer;
_logger = logger;
- _fileSystem = fileSystem;
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 3854be703..8439f8a99 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
@@ -25,11 +24,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class ChapterImagesTask : IScheduledTask
{
/// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger<ChapterImagesTask> _logger;
-
- /// <summary>
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
@@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary>
public ChapterImagesTask(
- ILoggerFactory loggerFactory,
ILibraryManager libraryManager,
IItemRepository itemRepo,
IApplicationPaths appPaths,
@@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
IFileSystem fileSystem,
ILocalizationManager localization)
{
- _logger = loggerFactory.CreateLogger<ChapterImagesTask>();
_libraryManager = libraryManager;
_itemRepo = itemRepo;
_appPaths = appPaths;
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 296822981..27024e4e1 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.Serialization
@@ -53,10 +54,11 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="stream">The stream.</param>
public void SerializeToStream(object obj, Stream stream)
{
- using (var writer = new XmlTextWriter(stream, null))
+ using (var writer = new StreamWriter(stream, null, IODefaults.StreamWriterBufferSize, true))
+ using (var textWriter = new XmlTextWriter(writer))
{
- writer.Formatting = Formatting.Indented;
- SerializeToWriter(obj, writer);
+ textWriter.Formatting = Formatting.Indented;
+ SerializeToWriter(obj, textWriter);
}
}
@@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Serialization
/// <returns>System.Object.</returns>
public object DeserializeFromBytes(Type type, byte[] buffer)
{
- using (var stream = new MemoryStream(buffer))
+ using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
{
return DeserializeFromStream(type, stream);
}
diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs
index 857df591a..47e7261e8 100644
--- a/Emby.Server.Implementations/Services/ServiceController.cs
+++ b/Emby.Server.Implementations/Services/ServiceController.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
@@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
{
if (restPath.Path[0] != '/')
{
- throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
+ throw new ArgumentException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Route '{0}' on '{1}' must start with a '/'",
+ restPath.Path,
+ restPath.RequestType.GetMethodName()));
}
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
{
- throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
+ throw new ArgumentException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Route '{0}' on '{1}' contains invalid chars. ",
+ restPath.Path,
+ restPath.RequestType.GetMethodName()));
}
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
@@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
var service = httpHost.CreateInstance(serviceType);
- var serviceRequiresContext = service as IRequiresRequest;
- if (serviceRequiresContext != null)
+ if (service is IRequiresRequest serviceRequiresContext)
{
serviceRequiresContext.Request = req;
}
@@ -189,5 +199,4 @@ namespace Emby.Server.Implementations.Services
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
}
}
-
}
diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs
index cbc4b754d..7b970627e 100644
--- a/Emby.Server.Implementations/Services/ServiceExec.cs
+++ b/Emby.Server.Implementations/Services/ServiceExec.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@@ -105,7 +106,13 @@ namespace Emby.Server.Implementations.Services
}
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
- throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName()));
+ throw new NotImplementedException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Could not find method named {1}({0}) or Any({0}) on Service {2}",
+ requestDto.GetType().GetMethodName(),
+ expectedMethodName,
+ serviceType.GetMethodName()));
}
private static async Task<object> GetTaskResult(Task task)
diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs
index a42f88ea0..b4166f771 100644
--- a/Emby.Server.Implementations/Services/ServiceHandler.cs
+++ b/Emby.Server.Implementations/Services/ServiceHandler.cs
@@ -2,10 +2,12 @@
using System;
using System.Collections.Generic;
+using System.Net.Mime;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -44,7 +46,7 @@ namespace Emby.Server.Implementations.Services
var pos = pathInfo.LastIndexOf('.');
if (pos != -1)
{
- var format = pathInfo.Substring(pos + 1);
+ var format = pathInfo.AsSpan().Slice(pos + 1);
contentType = GetFormatContentType(format);
if (contentType != null)
{
@@ -55,18 +57,21 @@ namespace Emby.Server.Implementations.Services
return pathInfo;
}
- private static string GetFormatContentType(string format)
+ private static string GetFormatContentType(ReadOnlySpan<char> format)
{
- // built-in formats
- switch (format)
+ if (format.Equals("json", StringComparison.Ordinal))
{
- case "json": return "application/json";
- case "xml": return "application/xml";
- default: return null;
+ return MediaTypeNames.Application.Json;
}
+ else if (format.Equals("xml", StringComparison.Ordinal))
+ {
+ return MediaTypeNames.Application.Xml;
+ }
+
+ return null;
}
- public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
+ public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
{
httpReq.Items["__route"] = _restPath;
@@ -75,10 +80,11 @@ namespace Emby.Server.Implementations.Services
httpReq.ResponseContentType = _responseContentType;
}
- var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
+ var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
+ httpRes.HttpContext.SetServiceStackRequest(httpReq);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters
@@ -90,7 +96,7 @@ namespace Emby.Server.Implementations.Services
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
}
- public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
+ public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
{
var requestType = restPath.RequestType;
diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs
index 89538ae72..442b2ab1c 100644
--- a/Emby.Server.Implementations/Services/ServicePath.cs
+++ b/Emby.Server.Implementations/Services/ServicePath.cs
@@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.Services
{
var component = components[i];
- if (component.StartsWith(VariablePrefix))
+ if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
{
var variableName = component.Substring(1, component.Length - 2);
if (variableName[variableName.Length - 1] == WildCardChar)
@@ -488,7 +488,8 @@ namespace Emby.Server.Implementations.Services
sb.Append(value);
for (var j = pathIx + 1; j < requestComponents.Length; j++)
{
- sb.Append(PathSeperatorChar + requestComponents[j]);
+ sb.Append(PathSeperatorChar)
+ .Append(requestComponents[j]);
}
value = sb.ToString();
@@ -505,7 +506,8 @@ namespace Emby.Server.Implementations.Services
pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
- sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
+ sb.Append(PathSeperatorChar)
+ .Append(requestComponents[pathIx++]);
}
value = sb.ToString();
diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs
deleted file mode 100644
index 4f011a678..000000000
--- a/Emby.Server.Implementations/Services/SwaggerService.cs
+++ /dev/null
@@ -1,287 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- [Route("/swagger", "GET", Summary = "Gets the swagger specifications")]
- [Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")]
- public class GetSwaggerSpec : IReturn<SwaggerSpec>
- {
- }
-
- public class SwaggerSpec
- {
- public string swagger { get; set; }
-
- public string[] schemes { get; set; }
-
- public SwaggerInfo info { get; set; }
-
- public string host { get; set; }
-
- public string basePath { get; set; }
-
- public SwaggerTag[] tags { get; set; }
-
- public IDictionary<string, Dictionary<string, SwaggerMethod>> paths { get; set; }
-
- public Dictionary<string, SwaggerDefinition> definitions { get; set; }
-
- public SwaggerComponents components { get; set; }
- }
-
- public class SwaggerComponents
- {
- public Dictionary<string, SwaggerSecurityScheme> securitySchemes { get; set; }
- }
-
- public class SwaggerSecurityScheme
- {
- public string name { get; set; }
-
- public string type { get; set; }
-
- public string @in { get; set; }
- }
-
- public class SwaggerInfo
- {
- public string description { get; set; }
-
- public string version { get; set; }
-
- public string title { get; set; }
-
- public string termsOfService { get; set; }
-
- public SwaggerConcactInfo contact { get; set; }
- }
-
- public class SwaggerConcactInfo
- {
- public string email { get; set; }
-
- public string name { get; set; }
-
- public string url { get; set; }
- }
-
- public class SwaggerTag
- {
- public string description { get; set; }
-
- public string name { get; set; }
- }
-
- public class SwaggerMethod
- {
- public string summary { get; set; }
-
- public string description { get; set; }
-
- public string[] tags { get; set; }
-
- public string operationId { get; set; }
-
- public string[] consumes { get; set; }
-
- public string[] produces { get; set; }
-
- public SwaggerParam[] parameters { get; set; }
-
- public Dictionary<string, SwaggerResponse> responses { get; set; }
-
- public Dictionary<string, string[]>[] security { get; set; }
- }
-
- public class SwaggerParam
- {
- public string @in { get; set; }
-
- public string name { get; set; }
-
- public string description { get; set; }
-
- public bool required { get; set; }
-
- public string type { get; set; }
-
- public string collectionFormat { get; set; }
- }
-
- public class SwaggerResponse
- {
- public string description { get; set; }
-
- // ex. "$ref":"#/definitions/Pet"
- public Dictionary<string, string> schema { get; set; }
- }
-
- public class SwaggerDefinition
- {
- public string type { get; set; }
-
- public Dictionary<string, SwaggerProperty> properties { get; set; }
- }
-
- public class SwaggerProperty
- {
- public string type { get; set; }
-
- public string format { get; set; }
-
- public string description { get; set; }
-
- public string[] @enum { get; set; }
-
- public string @default { get; set; }
- }
-
- public class SwaggerService : IService, IRequiresRequest
- {
- private readonly IHttpServer _httpServer;
- private SwaggerSpec _spec;
-
- public IRequest Request { get; set; }
-
- public SwaggerService(IHttpServer httpServer)
- {
- _httpServer = httpServer;
- }
-
- public object Get(GetSwaggerSpec request)
- {
- return _spec ?? (_spec = GetSpec());
- }
-
- private SwaggerSpec GetSpec()
- {
- string host = null;
- Uri uri;
- if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri))
- {
- host = uri.Host;
- }
-
- var securitySchemes = new Dictionary<string, SwaggerSecurityScheme>();
-
- securitySchemes["api_key"] = new SwaggerSecurityScheme
- {
- name = "api_key",
- type = "apiKey",
- @in = "query"
- };
-
- var spec = new SwaggerSpec
- {
- schemes = new[] { "http" },
- tags = GetTags(),
- swagger = "2.0",
- info = new SwaggerInfo
- {
- title = "Jellyfin Server API",
- version = "1.0.0",
- description = "Explore the Jellyfin Server API",
- contact = new SwaggerConcactInfo
- {
- name = "Jellyfin Community",
- url = "https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help/"
- }
- },
- paths = GetPaths(),
- definitions = GetDefinitions(),
- basePath = "/jellyfin",
- host = host,
-
- components = new SwaggerComponents
- {
- securitySchemes = securitySchemes
- }
- };
-
- return spec;
- }
-
-
- private SwaggerTag[] GetTags()
- {
- return Array.Empty<SwaggerTag>();
- }
-
- private Dictionary<string, SwaggerDefinition> GetDefinitions()
- {
- return new Dictionary<string, SwaggerDefinition>();
- }
-
- private IDictionary<string, Dictionary<string, SwaggerMethod>> GetPaths()
- {
- var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
-
- // REVIEW: this can be done better
- var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
-
- foreach (var current in all)
- {
- foreach (var info in current.Value)
- {
- if (info.IsHidden)
- {
- continue;
- }
-
- if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
- || info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- paths[info.Path] = GetPathInfo(info);
- }
- }
-
- return paths;
- }
-
- private Dictionary<string, SwaggerMethod> GetPathInfo(RestPath info)
- {
- var result = new Dictionary<string, SwaggerMethod>();
-
- foreach (var verb in info.Verbs)
- {
- var responses = new Dictionary<string, SwaggerResponse>
- {
- { "200", new SwaggerResponse { description = "OK" } }
- };
-
- var apiKeySecurity = new Dictionary<string, string[]>
- {
- { "api_key", Array.Empty<string>() }
- };
-
- result[verb.ToLowerInvariant()] = new SwaggerMethod
- {
- summary = info.Summary,
- description = info.Description,
- produces = new[] { "application/json" },
- consumes = new[] { "application/json" },
- operationId = info.RequestType.Name,
- tags = Array.Empty<string>(),
-
- parameters = Array.Empty<SwaggerParam>(),
-
- responses = responses,
-
- security = new[] { apiKeySecurity }
- };
- }
-
- return result;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index d069d1ada..862a7296c 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Session
}
catch (DbUpdateConcurrencyException e)
{
- _logger.LogWarning(e, "Error updating user's last activity date.");
+ _logger.LogDebug(e, "Error updating user's last activity date.");
}
}
}
@@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">info</exception>
- /// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
+ /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
public async Task OnPlaybackStopped(PlaybackStopInfo info)
{
CheckDisposed();
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index b9db6ecd0..8bebd37dc 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
if (session != null)
{
EnsureController(session, e.Argument);
- await KeepAliveWebSocket(e.Argument);
+ await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
}
else
{
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
// Notify WebSocket about timeout
try
{
- await SendForceKeepAlive(webSocket);
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
}
catch (WebSocketException exception)
{
@@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
if (_keepAliveCancellationToken != null)
{
_keepAliveCancellationToken.Cancel();
+ _keepAliveCancellationToken.Dispose();
_keepAliveCancellationToken = null;
}
}
@@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
}
- if (inactive.Any())
+ if (inactive.Count > 0)
{
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
}
@@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
{
try
{
- await SendForceKeepAlive(webSocket);
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
}
catch (WebSocketException exception)
{
@@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
lock (_webSocketsLock)
{
- if (lost.Any())
+ if (lost.Count > 0)
{
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
foreach (var webSocket in lost)
@@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
}
}
- if (!_webSockets.Any())
+ if (_webSockets.Count == 0)
{
StopKeepAlive();
}
@@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
/// <returns>Task.</returns>
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
{
- return webSocket.SendAsync(new WebSocketMessage<int>
- {
- MessageType = "ForceKeepAlive",
- Data = WebSocketLostTimeout
- }, CancellationToken.None);
+ return webSocket.SendAsync(
+ new WebSocketMessage<int>
+ {
+ MessageType = "ForceKeepAlive",
+ Data = WebSocketLostTimeout
+ },
+ CancellationToken.None);
}
/// <summary>
@@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
{
while (!cancellationToken.IsCancellationRequested)
{
- await callback();
- Task task = Task.Delay(interval, cancellationToken);
+ await callback().ConfigureAwait(false);
try
{
- await task;
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 2b7d818be..1f68a9c81 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
private static int CompareEpisodes(Episode x, Episode y)
{
- var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
- var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
+ var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
+ var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
return xValue.CompareTo(yValue);
}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
index b1f8fd330..80b977731 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
/// All sessions will receive the message.
/// </summary>
AllGroup = 0,
+
/// <summary>
/// Only the specified session will receive the message.
/// </summary>
CurrentSession = 1,
+
/// <summary>
/// All sessions, except the current one, will receive the message.
/// </summary>
AllExceptCurrentSession = 2,
+
/// <summary>
/// Only sessions that are not buffering will receive the message.
/// </summary>
@@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
/// </summary>
private readonly GroupInfo _group = new GroupInfo();
- /// <inheritdoc />
- public Guid GetGroupId() => _group.GroupId;
-
- /// <inheritdoc />
- public Guid GetPlayingItemId() => _group.PlayingItem.Id;
-
- /// <inheritdoc />
- public bool IsGroupEmpty() => _group.IsEmpty();
-
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
/// </summary>
@@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
_syncPlayManager = syncPlayManager;
}
+ /// <inheritdoc />
+ public Guid GetGroupId() => _group.GroupId;
+
+ /// <inheritdoc />
+ public Guid GetPlayingItemId() => _group.PlayingItem.Id;
+
+ /// <inheritdoc />
+ public bool IsGroupEmpty() => _group.IsEmpty();
+
/// <summary>
/// Converts DateTime to UTC string.
/// </summary>
@@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <value>The UTC string.</value>
private string DateToUTCString(DateTime date)
{
- return date.ToUniversalTime().ToString("o");
+ return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
}
/// <summary>
@@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
/// <param name="from">The current session.</param>
/// <param name="type">The filtering type.</param>
/// <value>The array of sessions matching the filter.</value>
- private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
+ private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
{
switch (type)
{
case BroadcastType.CurrentSession:
return new SessionInfo[] { from };
case BroadcastType.AllGroup:
- return _group.Participants.Values.Select(
- session => session.Session).ToArray();
+ return _group.Participants.Values
+ .Select(session => session.Session);
case BroadcastType.AllExceptCurrentSession:
- return _group.Participants.Values.Select(
- session => session.Session).Where(
- session => !session.Id.Equals(from.Id)).ToArray();
+ return _group.Participants.Values
+ .Select(session => session.Session)
+ .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
case BroadcastType.AllReady:
- return _group.Participants.Values.Where(
- session => !session.IsBuffering).Select(
- session => session.Session).ToArray();
+ return _group.Participants.Values
+ .Where(session => !session.IsBuffering)
+ .Select(session => session.Session);
default:
return Array.Empty<SessionInfo>();
}
@@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- SessionInfo[] sessions = FilterSessions(from, type);
- foreach (var session in sessions)
+ foreach (var session in FilterSessions(from, type))
{
- yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
}
}
@@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- SessionInfo[] sessions = FilterSessions(from, type);
- foreach (var session in sessions)
+ foreach (var session in FilterSessions(from, type))
{
- yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
}
}
@@ -194,26 +196,24 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
- public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
+ public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
{
_group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this);
_group.PlayingItem = session.FullNowPlayingItem;
- _group.IsPaused = true;
+ _group.IsPaused = session.PlayState.IsPaused;
_group.PositionTicks = session.PlayState.PositionTicks ?? 0;
_group.LastActivity = DateTime.UtcNow;
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
- var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
}
/// <inheritdoc />
public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
{
- if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
+ if (session.NowPlayingItem?.Id == _group.PlayingItem.Id)
{
_group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this);
@@ -224,7 +224,7 @@ namespace Emby.Server.Implementations.SyncPlay
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
- // Client join and play, syncing will happen client side
+ // Syncing will happen client-side
if (!_group.IsPaused)
{
var playCommand = NewSyncPlayCommand(SendCommandType.Play);
@@ -238,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
}
else
{
- var playRequest = new PlayRequest();
- playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
- playRequest.StartPositionTicks = _group.PositionTicks;
+ var playRequest = new PlayRequest
+ {
+ ItemIds = new Guid[] { _group.PlayingItem.Id },
+ StartPositionTicks = _group.PositionTicks
+ };
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
}
@@ -262,10 +264,9 @@ namespace Emby.Server.Implementations.SyncPlay
/// <inheritdoc />
public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
{
- // The server's job is to mantain a consistent state to which clients refer to,
- // as also to notify clients of state changes.
- // The actual syncing of media playback happens client side.
- // Clients are aware of the server's time and use it to sync.
+ // The server's job is to maintain a consistent state for clients to reference
+ // and notify clients of state changes. The actual syncing of media playback
+ // happens client side. Clients are aware of the server's time and use it to sync.
switch (request.Type)
{
case PlaybackRequestType.Play:
@@ -277,13 +278,13 @@ namespace Emby.Server.Implementations.SyncPlay
case PlaybackRequestType.Seek:
HandleSeekRequest(session, request, cancellationToken);
break;
- case PlaybackRequestType.Buffering:
+ case PlaybackRequestType.Buffer:
HandleBufferingRequest(session, request, cancellationToken);
break;
- case PlaybackRequestType.BufferingDone:
+ case PlaybackRequestType.Ready:
HandleBufferingDoneRequest(session, request, cancellationToken);
break;
- case PlaybackRequestType.UpdatePing:
+ case PlaybackRequestType.Ping:
HandlePingUpdateRequest(session, request);
break;
}
@@ -301,7 +302,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
// Pick a suitable time that accounts for latency
var delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+ delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
// Unpause group and set starting point in future
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
@@ -337,8 +338,9 @@ namespace Emby.Server.Implementations.SyncPlay
var currentTime = DateTime.UtcNow;
var elapsedTime = currentTime - _group.LastActivity;
_group.LastActivity = currentTime;
+
// Seek only if playback actually started
- // (a pause request may be issued during the delay added to account for latency)
+ // Pause request may be issued during the delay added to account for latency
_group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
var command = NewSyncPlayCommand(SendCommandType.Pause);
@@ -451,7 +453,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
// Client, that was buffering, resumed playback but did not update others in time
delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+ delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
_group.LastActivity = currentTime.AddMilliseconds(
delay);
@@ -495,7 +497,7 @@ namespace Emby.Server.Implementations.SyncPlay
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
{
// Collected pings are used to account for network latency when unpausing playback
- _group.UpdatePing(session, request.Ping ?? _group.DefaulPing);
+ _group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 45a43fd78..966ed5024 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -170,10 +170,11 @@ namespace Emby.Server.Implementations.SyncPlay
{
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
- var error = new GroupUpdate<string>()
+ var error = new GroupUpdate<string>
{
Type = GroupUpdateType.CreateGroupDenied
};
+
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay
var group = new SyncPlayController(_sessionManager, this);
_groups[group.GetGroupId()] = group;
- group.InitGroup(session, cancellationToken);
+ group.CreateGroup(session, cancellationToken);
}
}
@@ -205,6 +206,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
+
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -300,9 +302,9 @@ namespace Emby.Server.Implementations.SyncPlay
group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
group => group.GetInfo()).ToList();
}
- // Otherwise show all available groups
else
{
+ // Otherwise show all available groups
return _groups.Values.Where(
group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
group => group.GetInfo()).ToList();
@@ -322,6 +324,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
+
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -366,7 +369,6 @@ namespace Emby.Server.Implementations.SyncPlay
}
_sessionToGroupMap.Remove(session.Id, out var tempGroup);
-
if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
{
throw new InvalidOperationException("Session was in wrong group!");
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 21c12ae79..d1818deff 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10;
}
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { typeof(Episode).Name },
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
- SeriesPresentationUniqueKey = presentationUniqueKey,
- Limit = limit,
- DtoOptions = new DtoOptions
- {
- Fields = new ItemFields[]
+ var items = _libraryManager
+ .GetItemList(
+ new InternalItemsQuery(user)
{
- ItemFields.SeriesPresentationUniqueKey
- },
- EnableImages = false
- },
- GroupBySeriesPresentationUniqueKey = true
-
- }, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey);
+ IncludeItemTypes = new[] { typeof(Episode).Name },
+ OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
+ SeriesPresentationUniqueKey = presentationUniqueKey,
+ Limit = limit,
+ DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
+ GroupBySeriesPresentationUniqueKey = true
+ }, parentsFolders.ToList())
+ .Cast<Episode>()
+ .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
+ .Select(GetUniqueSeriesKey);
// Avoid implicitly captured closure
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 146ebaf25..4f54c06dd 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -148,6 +148,11 @@ namespace Emby.Server.Implementations.Updates
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
+ catch (HttpRequestException ex)
+ {
+ _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
+ }
}
/// <inheritdoc />
diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
new file mode 100644
index 000000000..2fdd1e489
--- /dev/null
+++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.Routing;
+
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Identifies an action that supports the HTTP GET method.
+ /// </summary>
+ public class HttpSubscribeAttribute : HttpMethodAttribute
+ {
+ 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)
+ {
+ if (template == null)
+ {
+ throw new ArgumentNullException(nameof(template));
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
new file mode 100644
index 000000000..d6d7e4563
--- /dev/null
+++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.Routing;
+
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Identifies an action that supports the HTTP GET method.
+ /// </summary>
+ public class HttpUnsubscribeAttribute : HttpMethodAttribute
+ {
+ 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)
+ {
+ if (template == null)
+ {
+ throw new ArgumentNullException(nameof(template));
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
new file mode 100644
index 000000000..aa366f567
--- /dev/null
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -0,0 +1,103 @@
+using System.Security.Claims;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth
+{
+ /// <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)
+ {
+ // Ensure claim has userId.
+ var userId = ClaimHelpers.GetUserId(claimsPrincipal);
+ if (!userId.HasValue)
+ {
+ return false;
+ }
+
+ // Ensure userId links to a valid user.
+ var user = _userManager.GetUserById(userId.Value);
+ if (user == null)
+ {
+ return false;
+ }
+
+ // Ensure user is not disabled.
+ if (user.HasPermission(PermissionKind.IsDisabled))
+ {
+ return false;
+ }
+
+ var ip = RequestHelpers.NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
+ var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
+ // User cannot access remotely and user is remote
+ if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
+ {
+ return false;
+ }
+
+ if (localAccessOnly && !isInLocalNetwork)
+ {
+ return false;
+ }
+
+ // User attempting to access out of parental control hours.
+ if (!ignoreSchedule
+ && !user.HasPermission(PermissionKind.IsAdministrator)
+ && !user.IsParentalScheduleAllowed())
+ {
+ return false;
+ }
+
+ // User attempting to download without permission.
+ if (requiredDownloadPermission
+ && !user.HasPermission(PermissionKind.EnableContentDownloading))
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index f86f75b1c..733c6959e 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Security.Authentication;
using System.Security.Claims;
using System.Text.Encodings.Web;
@@ -44,14 +45,24 @@ namespace Jellyfin.Api.Auth
var authorizationInfo = _authService.Authenticate(Request);
if (authorizationInfo == null)
{
- return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
+ return Task.FromResult(AuthenticateResult.NoResult());
+ // TODO return when legacy API is removed.
+ // Don't spam the log with "Invalid User"
+ // return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
}
var claims = new[]
{
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
- new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
+ new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
+ new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
+ new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
+ new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
+ new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
+ new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
};
+
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
new file mode 100644
index 000000000..b5913daab
--- /dev/null
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
+{
+ /// <summary>
+ /// Default authorization handler.
+ /// </summary>
+ public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> 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 DefaultAuthorizationHandler(
+ IUserManager userManager,
+ INetworkManager networkManager,
+ IHttpContextAccessor httpContextAccessor)
+ : base(userManager, networkManager, httpContextAccessor)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
+ {
+ var validated = ValidateClaims(context.User);
+ if (!validated)
+ {
+ context.Fail();
+ return Task.CompletedTask;
+ }
+
+ context.Succeed(requirement);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
new file mode 100644
index 000000000..7cea00b69
--- /dev/null
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
+{
+ /// <summary>
+ /// The default authorization requirement.
+ /// </summary>
+ public class DefaultAuthorizationRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
new file mode 100644
index 000000000..b61680ab1
--- /dev/null
+++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.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
new file mode 100644
index 000000000..b0a72a9de
--- /dev/null
+++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
@@ -0,0 +1,11 @@
+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
new file mode 100644
index 000000000..31482a930
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
@@ -0,0 +1,56 @@
+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
new file mode 100644
index 000000000..00aaec334
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
@@ -0,0 +1,11 @@
+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
new file mode 100644
index 000000000..9815e252e
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
@@ -0,0 +1,56 @@
+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 firstTimeSetupOrDefaultRequirement)
+ {
+ if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+ {
+ context.Succeed(firstTimeSetupOrDefaultRequirement);
+ return Task.CompletedTask;
+ }
+
+ var validated = ValidateClaims(context.User);
+ if (validated)
+ {
+ context.Succeed(firstTimeSetupOrDefaultRequirement);
+ }
+ else
+ {
+ context.Fail();
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
new file mode 100644
index 000000000..f7366bd7a
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
@@ -0,0 +1,11 @@
+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/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
index 34aa5d12c..decbe0c03 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -1,22 +1,33 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
{
/// <summary>
/// Authorization handler for requiring first time setup or elevated privileges.
/// </summary>
- public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
+ public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
{
private readonly IConfigurationManager _configurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
/// </summary>
- /// <param name="configurationManager">The jellyfin configuration manager.</param>
- public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
+ /// <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(
+ IConfigurationManager configurationManager,
+ IUserManager userManager,
+ INetworkManager networkManager,
+ IHttpContextAccessor httpContextAccessor)
+ : base(userManager, networkManager, httpContextAccessor)
{
_configurationManager = configurationManager;
}
@@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
+ return Task.CompletedTask;
}
- else if (context.User.IsInRole(UserRoles.Administrator))
+
+ var validated = ValidateClaims(context.User);
+ if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
new file mode 100644
index 000000000..5213bc4cb
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.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.Fail();
+ return Task.CompletedTask;
+ }
+
+ context.Succeed(requirement);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
new file mode 100644
index 000000000..cdad74270
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
@@ -0,0 +1,11 @@
+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
new file mode 100644
index 000000000..14722aa57
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
@@ -0,0 +1,45 @@
+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.LocalAccessOrRequiresElevationPolicy
+{
+ /// <summary>
+ /// Local access or require elevated privileges handler.
+ /// </summary>
+ public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
+ {
+ /// <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)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
+ {
+ var validated = ValidateClaims(context.User, localAccessOnly: true);
+ if (validated || context.User.IsInRole(UserRoles.Administrator))
+ {
+ context.Succeed(requirement);
+ }
+ else
+ {
+ context.Fail();
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
new file mode 100644
index 000000000..d9c64d01c
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
+{
+ /// <summary>
+ /// The local access or elevated privileges authorization requirement.
+ /// </summary>
+ public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
new file mode 100644
index 000000000..af73352bc
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.LocalAccessPolicy
+{
+ /// <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.Fail();
+ }
+ else
+ {
+ context.Succeed(requirement);
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
new file mode 100644
index 000000000..761127fa4
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
@@ -0,0 +1,11 @@
+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
index 2d3bb1aa4..b235c4b63 100644
--- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
+++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
@@ -1,21 +1,43 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
{
/// <summary>
/// Authorization handler for requiring elevated privileges.
/// </summary>
- public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
+ 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)
{
- if (context.User.IsInRole(UserRoles.Administrator))
+ var validated = ValidateClaims(context.User);
+ if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
}
+ else
+ {
+ context.Fail();
+ }
return Task.CompletedTask;
}
diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs
new file mode 100644
index 000000000..4d7c7135d
--- /dev/null
+++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs
@@ -0,0 +1,38 @@
+namespace Jellyfin.Api.Constants
+{
+ /// <summary>
+ /// Internal claim types for authorization.
+ /// </summary>
+ public static class InternalClaimTypes
+ {
+ /// <summary>
+ /// User Id.
+ /// </summary>
+ public const string UserId = "Jellyfin-UserId";
+
+ /// <summary>
+ /// Device Id.
+ /// </summary>
+ public const string DeviceId = "Jellyfin-DeviceId";
+
+ /// <summary>
+ /// Device.
+ /// </summary>
+ public const string Device = "Jellyfin-Device";
+
+ /// <summary>
+ /// Client.
+ /// </summary>
+ public const string Client = "Jellyfin-Client";
+
+ /// <summary>
+ /// Version.
+ /// </summary>
+ public const string Version = "Jellyfin-Version";
+
+ /// <summary>
+ /// Token.
+ /// </summary>
+ public const string Token = "Jellyfin-Token";
+ }
+}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index e2b383f75..7d7767470 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -6,13 +6,48 @@ namespace Jellyfin.Api.Constants
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 = "FirstTimeOrElevated";
+ 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 escaping schedule controls or requiring first time setup.
+ /// </summary>
+ public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
}
}
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
new file mode 100644
index 000000000..a07cea9c0
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Activity log controller.
+ /// </summary>
+ [Route("System/ActivityLog")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public class ActivityLogController : BaseJellyfinApiController
+ {
+ private readonly IActivityManager _activityManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ActivityLogController"/> class.
+ /// </summary>
+ /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
+ public ActivityLogController(IActivityManager activityManager)
+ {
+ _activityManager = activityManager;
+ }
+
+ /// <summary>
+ /// Gets activity log entries.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
+ /// <param name="hasUserId">Optional. 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 ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] DateTime? minDate,
+ [FromQuery] bool? hasUserId)
+ {
+ var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
+ entries => entries.Where(entry => entry.DateCreated >= minDate
+ && (!hasUserId.HasValue || (hasUserId.Value
+ ? entry.UserId != Guid.Empty
+ : entry.UserId == Guid.Empty))));
+
+ return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
new file mode 100644
index 000000000..190d4bd07
--- /dev/null
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The albums controller.
+ /// </summary>
+ [Route("")]
+ public class AlbumsController : BaseJellyfinApiController
+ {
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AlbumsController"/> 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 AlbumsController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService)
+ {
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ }
+
+ /// <summary>
+ /// Finds albums similar to a given album.
+ /// </summary>
+ /// <param name="albumId">The album id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <response code="200">Similar albums returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
+ [HttpGet("Albums/{albumId}/Similar")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
+ [FromRoute] string albumId,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? excludeArtistIds,
+ [FromQuery] int? limit)
+ {
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ return SimilarItemsHelper.GetSimilarItemsResult(
+ dtoOptions,
+ _userManager,
+ _libraryManager,
+ _dtoService,
+ userId,
+ albumId,
+ excludeArtistIds,
+ limit,
+ new[] { typeof(MusicAlbum) },
+ GetAlbumSimilarityScore);
+ }
+
+ /// <summary>
+ /// Finds artists similar to a given artist.
+ /// </summary>
+ /// <param name="artistId">The artist id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <response code="200">Similar artists returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
+ [HttpGet("Artists/{artistId}/Similar")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
+ [FromRoute] string artistId,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? excludeArtistIds,
+ [FromQuery] int? limit)
+ {
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ return SimilarItemsHelper.GetSimilarItemsResult(
+ dtoOptions,
+ _userManager,
+ _libraryManager,
+ _dtoService,
+ userId,
+ artistId,
+ excludeArtistIds,
+ limit,
+ new[] { typeof(MusicArtist) },
+ SimilarItemsHelper.GetSimiliarityScore);
+ }
+
+ /// <summary>
+ /// Gets a similairty score of two albums.
+ /// </summary>
+ /// <param name="item1">The first item.</param>
+ /// <param name="item1People">The item1 people.</param>
+ /// <param name="allPeople">All people.</param>
+ /// <param name="item2">The second item.</param>
+ /// <returns>System.Int32.</returns>
+ private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
+ {
+ var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
+
+ var album1 = (MusicAlbum)item1;
+ var album2 = (MusicAlbum)item2;
+
+ var artists1 = album1
+ .GetAllArtists()
+ .DistinctNames()
+ .ToList();
+
+ var artists2 = new HashSet<string>(
+ album2.GetAllArtists().DistinctNames(),
+ StringComparer.OrdinalIgnoreCase);
+
+ return points + artists1.Where(artists2.Contains).Sum(i => 5);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
new file mode 100644
index 000000000..0e28d4c47
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -0,0 +1,97 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Authentication controller.
+ /// </summary>
+ [Route("Auth")]
+ public class ApiKeyController : BaseJellyfinApiController
+ {
+ private readonly ISessionManager _sessionManager;
+ private readonly IServerApplicationHost _appHost;
+ private readonly IAuthenticationRepository _authRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ApiKeyController"/> class.
+ /// </summary>
+ /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+ /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
+ public ApiKeyController(
+ ISessionManager sessionManager,
+ IServerApplicationHost appHost,
+ IAuthenticationRepository authRepo)
+ {
+ _sessionManager = sessionManager;
+ _appHost = appHost;
+ _authRepo = authRepo;
+ }
+
+ /// <summary>
+ /// Get all keys.
+ /// </summary>
+ /// <response code="200">Api keys retrieved.</response>
+ /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
+ [HttpGet("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<AuthenticationInfo>> GetKeys()
+ {
+ var result = _authRepo.Get(new AuthenticationInfoQuery
+ {
+ HasUser = false
+ });
+
+ return result;
+ }
+
+ /// <summary>
+ /// Create a new api key.
+ /// </summary>
+ /// <param name="app">Name of the app using the authentication key.</param>
+ /// <response code="204">Api key created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CreateKey([FromQuery, Required] string? app)
+ {
+ _authRepo.Create(new AuthenticationInfo
+ {
+ AppName = app,
+ AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
+ DateCreated = DateTime.UtcNow,
+ DeviceId = _appHost.SystemId,
+ DeviceName = _appHost.FriendlyName,
+ AppVersion = _appHost.ApplicationVersionString
+ });
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Remove an api key.
+ /// </summary>
+ /// <param name="key">The access token to delete.</param>
+ /// <response code="204">Api key deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Keys/{key}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult RevokeKey([FromRoute, Required] string? key)
+ {
+ _sessionManager.RevokeToken(key);
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
new file mode 100644
index 000000000..3f72830cd
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -0,0 +1,488 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The artists controller.
+ /// </summary>
+ [Route("Artists")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class ArtistsController : BaseJellyfinApiController
+ {
+ 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. 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 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. 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="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="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] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? genres,
+ [FromQuery] string? genreIds,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? studioIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem;
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ user = _userManager.GetUserById(userId.Value);
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+ }
+ else
+ {
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+ }
+
+ var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+ var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+ var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypesArr,
+ IncludeItemTypes = includeItemTypesArr,
+ MediaTypes = mediaTypesArr,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = RequestHelpers.Split(tags, ',', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
+ Genres = RequestHelpers.Split(genres, ',', true),
+ GenreIds = RequestHelpers.GetGuids(genreIds),
+ StudioIds = RequestHelpers.GetGuids(studioIds),
+ Person = person,
+ PersonIds = RequestHelpers.GetGuids(personIds),
+ PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+ Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ if (parentItem is Folder)
+ {
+ query.AncestorIds = new[] { new Guid(parentId) };
+ }
+ else
+ {
+ query.ItemIds = new[] { new Guid(parentId) };
+ }
+ }
+
+ // Studios
+ if (!string.IsNullOrEmpty(studios))
+ {
+ query.StudioIds = studios.Split('|').Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(i);
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != null).Select(i => i!.Id).ToArray();
+ }
+
+ foreach (var filter in RequestHelpers.GetFilters(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.GetArtists(query);
+
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, itemCounts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (!string.IsNullOrWhiteSpace(includeItemTypes))
+ {
+ 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;
+ });
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
+
+ /// <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. 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 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. 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="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="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] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? genres,
+ [FromQuery] string? genreIds,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? studioIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem;
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ user = _userManager.GetUserById(userId.Value);
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+ }
+ else
+ {
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+ }
+
+ var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+ var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+ var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypesArr,
+ IncludeItemTypes = includeItemTypesArr,
+ MediaTypes = mediaTypesArr,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = RequestHelpers.Split(tags, ',', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
+ Genres = RequestHelpers.Split(genres, ',', true),
+ GenreIds = RequestHelpers.GetGuids(genreIds),
+ StudioIds = RequestHelpers.GetGuids(studioIds),
+ Person = person,
+ PersonIds = RequestHelpers.GetGuids(personIds),
+ PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+ Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ if (parentItem is Folder)
+ {
+ query.AncestorIds = new[] { new Guid(parentId) };
+ }
+ else
+ {
+ query.ItemIds = new[] { new Guid(parentId) };
+ }
+ }
+
+ // Studios
+ if (!string.IsNullOrEmpty(studios))
+ {
+ query.StudioIds = studios.Split('|').Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(i);
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != null).Select(i => i!.Id).ToArray();
+ }
+
+ foreach (var filter in RequestHelpers.GetFilters(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 dtos = result.Items.Select(i =>
+ {
+ var (baseItem, itemCounts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (!string.IsNullOrWhiteSpace(includeItemTypes))
+ {
+ 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;
+ });
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
+
+ /// <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] string name, [FromQuery] Guid? userId)
+ {
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ var item = _libraryManager.GetArtist(name, dtoOptions);
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ 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
new file mode 100644
index 000000000..802cd026e
--- /dev/null
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+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>
+ /// 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;
+ }
+
+ /// <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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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")]
+ [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
+ [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult> GetAudioStream(
+ [FromRoute] Guid itemId,
+ [FromRoute] 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string>? streamOptions)
+ {
+ StreamingRequestDto streamingRequest = new StreamingRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Static,
+ StreamOptions = streamOptions
+ };
+
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
new file mode 100644
index 000000000..1d4836f27
--- /dev/null
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -0,0 +1,57 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Branding;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Branding controller.
+ /// </summary>
+ public class BrandingController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
new file mode 100644
index 000000000..bdd7dfd96
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Channels Controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class ChannelsController : BaseJellyfinApiController
+ {
+ private readonly IChannelManager _channelManager;
+ private readonly IUserManager _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)
+ {
+ _channelManager = channelManager;
+ _userManager = 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 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 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] string 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. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</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] Guid channelId,
+ [FromQuery] Guid? folderId,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? sortOrder,
+ [FromQuery] string? filters,
+ [FromQuery] string? sortBy,
+ [FromQuery] string? fields)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var query = new InternalItemsQuery(user)
+ {
+ Limit = limit,
+ StartIndex = startIndex,
+ ChannelIds = new[] { channelId },
+ ParentId = folderId ?? Guid.Empty,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+ DtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ };
+
+ foreach (var filter in RequestHelpers.GetFilters(filters))
+ {
+ switch (filter)
+ {
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
+ }
+ }
+
+ return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <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. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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="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] string? filters,
+ [FromQuery] string? fields,
+ [FromQuery] string? channelIds)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var query = new InternalItemsQuery(user)
+ {
+ Limit = limit,
+ StartIndex = startIndex,
+ ChannelIds = (channelIds ?? string.Empty)
+ .Split(',')
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .Select(i => new Guid(i))
+ .ToArray(),
+ DtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ };
+
+ foreach (var filter in RequestHelpers.GetFilters(filters))
+ {
+ switch (filter)
+ {
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
+ }
+ }
+
+ return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
new file mode 100644
index 000000000..53821a188
--- /dev/null
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -0,0 +1,111 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Collections;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The collection controller.
+ /// </summary>
+ [Route("Collections")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class CollectionController : BaseJellyfinApiController
+ {
+ private readonly ICollectionManager _collectionManager;
+ private readonly IDtoService _dtoService;
+ private readonly IAuthorizationContext _authContext;
+
+ /// <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>
+ /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
+ public CollectionController(
+ ICollectionManager collectionManager,
+ IDtoService dtoService,
+ IAuthorizationContext authContext)
+ {
+ _collectionManager = collectionManager;
+ _dtoService = dtoService;
+ _authContext = authContext;
+ }
+
+ /// <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 ActionResult<CollectionCreationResult> CreateCollection(
+ [FromQuery] string? name,
+ [FromQuery] string? ids,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool isLocked = false)
+ {
+ var userId = _authContext.GetAuthorizationInfo(Request).UserId;
+
+ var item = _collectionManager.CreateCollection(new CollectionCreationOptions
+ {
+ IsLocked = isLocked,
+ Name = name,
+ ParentId = parentId,
+ ItemIdList = RequestHelpers.Split(ids, ',', true),
+ UserIds = new[] { userId }
+ });
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ 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="itemIds">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 ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
+ {
+ _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Removes items from a collection.
+ /// </summary>
+ /// <param name="collectionId">The collection id.</param>
+ /// <param name="itemIds">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 ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
+ {
+ _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
new file mode 100644
index 000000000..019703dae
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -0,0 +1,126 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.ConfigurationDtos;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Configuration;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Configuration Controller.
+ /// </summary>
+ [Route("System")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class ConfigurationController : BaseJellyfinApiController
+ {
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+
+ private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+
+ /// <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>
+ /// 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>
+ /// 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 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)]
+ public ActionResult<object> GetNamedConfiguration([FromRoute] string? key)
+ {
+ return _configurationManager.GetConfiguration(key);
+ }
+
+ /// <summary>
+ /// Updates named configuration.
+ /// </summary>
+ /// <param name="key">Configuration key.</param>
+ /// <response code="204">Named configuration updated.</response>
+ /// <returns>Update status.</returns>
+ [HttpPost("Configuration/{key}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
+ {
+ var configurationType = _configurationManager.GetConfigurationType(key);
+ var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
+ _configurationManager.SaveConfiguration(key, configuration);
+ 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>
+ /// 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([FromForm, 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
new file mode 100644
index 000000000..33abe3ccd
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -0,0 +1,273 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Jellyfin.Api.Models;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Plugins;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The dashboard controller.
+ /// </summary>
+ [Route("")]
+ public class DashboardController : BaseJellyfinApiController
+ {
+ private readonly ILogger<DashboardController> _logger;
+ private readonly IServerApplicationHost _appHost;
+ private readonly IConfiguration _appConfig;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IResourceFileManager _resourceFileManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DashboardController"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
+ /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param>
+ /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ public DashboardController(
+ ILogger<DashboardController> logger,
+ IServerApplicationHost appHost,
+ IConfiguration appConfig,
+ IResourceFileManager resourceFileManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _logger = logger;
+ _appHost = appHost;
+ _appConfig = appConfig;
+ _resourceFileManager = resourceFileManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <summary>
+ /// Gets the path of the directory containing the static web interface content, or null if the server is not
+ /// hosting the web client.
+ /// </summary>
+ private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager);
+
+ /// <summary>
+ /// Gets the configuration pages.
+ /// </summary>
+ /// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
+ /// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param>
+ /// <response code="200">ConfigurationPages returned.</response>
+ /// <response code="404">Server still loading.</response>
+ /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
+ [HttpGet("web/ConfigurationPages")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
+ [FromQuery] bool? enableInMainMenu,
+ [FromQuery] ConfigurationPageType? pageType)
+ {
+ const string unavailableMessage = "The server is still loading. Please try again momentarily.";
+
+ var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList();
+
+ if (pages == null)
+ {
+ return NotFound(unavailableMessage);
+ }
+
+ // Don't allow a failing plugin to fail them all
+ var configPages = pages.Select(p =>
+ {
+ try
+ {
+ return new ConfigurationPageInfo(p);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name);
+ return null;
+ }
+ })
+ .Where(i => i != null)
+ .ToList();
+
+ configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
+
+ if (pageType.HasValue)
+ {
+ configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList();
+ }
+
+ if (enableInMainMenu.HasValue)
+ {
+ configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
+ }
+
+ return configPages;
+ }
+
+ /// <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)]
+ public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
+ {
+ IPlugin? plugin = null;
+ Stream? stream = null;
+
+ var isJs = false;
+ var isTemplate = false;
+
+ var page = _appHost.GetExports<IPluginConfigurationPage>().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
+ if (page != null)
+ {
+ plugin = page.Plugin;
+ stream = page.GetHtmlStream();
+ }
+
+ if (plugin == null)
+ {
+ var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
+ if (altPage != null)
+ {
+ plugin = altPage.Item2;
+ stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
+
+ isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
+ isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
+ }
+ }
+
+ if (plugin != null && stream != null)
+ {
+ if (isJs)
+ {
+ return File(stream, MimeTypes.GetMimeType("page.js"));
+ }
+
+ if (isTemplate)
+ {
+ return File(stream, MimeTypes.GetMimeType("page.html"));
+ }
+
+ return File(stream, MimeTypes.GetMimeType("page.html"));
+ }
+
+ return NotFound();
+ }
+
+ /// <summary>
+ /// Gets the robots.txt.
+ /// </summary>
+ /// <response code="200">Robots.txt returned.</response>
+ /// <returns>The robots.txt.</returns>
+ [HttpGet("robots.txt")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public ActionResult GetRobotsTxt()
+ {
+ return GetWebClientResource("robots.txt");
+ }
+
+ /// <summary>
+ /// Gets a resource from the web client.
+ /// </summary>
+ /// <param name="resourceName">The resource name.</param>
+ /// <response code="200">Web client returned.</response>
+ /// <response code="404">Server does not host a web client.</response>
+ /// <returns>The resource.</returns>
+ [HttpGet("web/{*resourceName}")]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult GetWebClientResource([FromRoute] string resourceName)
+ {
+ if (!_appConfig.HostWebClient() || WebClientUiPath == null)
+ {
+ return NotFound("Server does not host a web client.");
+ }
+
+ var path = resourceName;
+ var basePath = WebClientUiPath;
+
+ var requestPathAndQuery = Request.GetEncodedPathAndQuery();
+ // Bounce them to the startup wizard if it hasn't been completed yet
+ if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
+ && !requestPathAndQuery.Contains("wizard", StringComparison.OrdinalIgnoreCase)
+ && requestPathAndQuery.Contains("index", StringComparison.OrdinalIgnoreCase))
+ {
+ return Redirect("index.html?start=wizard#!/wizardstart.html");
+ }
+
+ var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read);
+ return File(stream, MimeTypes.GetMimeType(path));
+ }
+
+ /// <summary>
+ /// Gets the favicon.
+ /// </summary>
+ /// <response code="200">Favicon.ico returned.</response>
+ /// <returns>The favicon.</returns>
+ [HttpGet("favicon.ico")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public ActionResult GetFavIcon()
+ {
+ return GetWebClientResource("favicon.ico");
+ }
+
+ /// <summary>
+ /// Gets the path of the directory containing the static web interface content.
+ /// </summary>
+ /// <param name="appConfig">The app configuration.</param>
+ /// <param name="serverConfigManager">The server configuration manager.</param>
+ /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
+ public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
+ {
+ if (!appConfig.HostWebClient())
+ {
+ return null;
+ }
+
+ if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
+ {
+ return serverConfigManager.Configuration.DashboardSourcePath;
+ }
+
+ return serverConfigManager.ApplicationPaths.WebPath;
+ }
+
+ private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
+ {
+ return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
+ }
+
+ private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
+ {
+ if (!(plugin is IHasWebPages hasWebPages))
+ {
+ return new List<Tuple<PluginPageInfo, IPlugin>>();
+ }
+
+ return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
+ }
+
+ private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
+ {
+ return _appHost.Plugins.SelectMany(GetPluginPages);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
new file mode 100644
index 000000000..1aed20ade
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -0,0 +1,155 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Devices Controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class DevicesController : BaseJellyfinApiController
+ {
+ private readonly IDeviceManager _deviceManager;
+ private readonly IAuthenticationRepository _authenticationRepository;
+ private readonly ISessionManager _sessionManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DevicesController"/> class.
+ /// </summary>
+ /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
+ /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+ public DevicesController(
+ IDeviceManager deviceManager,
+ IAuthenticationRepository authenticationRepository,
+ ISessionManager sessionManager)
+ {
+ _deviceManager = deviceManager;
+ _authenticationRepository = authenticationRepository;
+ _sessionManager = sessionManager;
+ }
+
+ /// <summary>
+ /// Get Devices.
+ /// </summary>
+ /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
+ /// <param name="userId">Gets or sets the user identifier.</param>
+ /// <response code="200">Devices retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
+ [HttpGet]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+ {
+ var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
+ return _deviceManager.GetDevices(deviceQuery);
+ }
+
+ /// <summary>
+ /// Get info for a device.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <response code="200">Device info retrieved.</response>
+ /// <response code="404">Device not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ [HttpGet("Info")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string? id)
+ {
+ var deviceInfo = _deviceManager.GetDevice(id);
+ if (deviceInfo == null)
+ {
+ return NotFound();
+ }
+
+ return deviceInfo;
+ }
+
+ /// <summary>
+ /// Get options for a device.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <response code="200">Device options retrieved.</response>
+ /// <response code="404">Device not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ [HttpGet("Options")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string? id)
+ {
+ var deviceInfo = _deviceManager.GetDeviceOptions(id);
+ if (deviceInfo == null)
+ {
+ return NotFound();
+ }
+
+ return deviceInfo;
+ }
+
+ /// <summary>
+ /// Update device options.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <param name="deviceOptions">Device Options.</param>
+ /// <response code="204">Device options updated.</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>
+ [HttpPost("Options")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult UpdateDeviceOptions(
+ [FromQuery, Required] string? id,
+ [FromBody, Required] DeviceOptions deviceOptions)
+ {
+ var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
+ if (existingDeviceOptions == null)
+ {
+ return NotFound();
+ }
+
+ _deviceManager.UpdateDeviceOptions(id, deviceOptions);
+ return NoContent();
+ }
+
+ /// <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 ActionResult DeleteDevice([FromQuery, Required] string? id)
+ {
+ var existingDevice = _deviceManager.GetDevice(id);
+ if (existingDevice == null)
+ {
+ return NotFound();
+ }
+
+ var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
+
+ foreach (var session in sessions)
+ {
+ _sessionManager.Logout(session);
+ }
+
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
new file mode 100644
index 000000000..c547d0cde
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -0,0 +1,176 @@
+using System;
+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.Controller;
+using MediaBrowser.Model.Entities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Display Preferences Controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class DisplayPreferencesController : BaseJellyfinApiController
+ {
+ private readonly IDisplayPreferencesManager _displayPreferencesManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
+ /// </summary>
+ /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
+ public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
+ {
+ _displayPreferencesManager = displayPreferencesManager;
+ }
+
+ /// <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] string? displayPreferencesId,
+ [FromQuery] [Required] Guid userId,
+ [FromQuery] [Required] string? client)
+ {
+ var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
+ var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
+
+ var dto = new DisplayPreferencesDto
+ {
+ Client = displayPreferences.Client,
+ Id = displayPreferences.UserId.ToString(),
+ ViewType = itemPreferences.ViewType.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();
+ }
+
+ foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
+ {
+ dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
+ }
+
+ dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
+ dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
+ dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
+ dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
+ dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+
+ 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] string? displayPreferencesId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery, Required] string? client,
+ [FromBody, Required] DisplayPreferencesDto displayPreferences)
+ {
+ HomeSectionType[] defaults =
+ {
+ HomeSectionType.SmallLibraryTiles,
+ HomeSectionType.Resume,
+ HomeSectionType.ResumeAudio,
+ HomeSectionType.LiveTv,
+ HomeSectionType.NextUp,
+ HomeSectionType.LatestMedia, HomeSectionType.None,
+ };
+
+ var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
+ existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
+ existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
+ existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
+
+ existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
+ existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
+ ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
+ : ChromecastVersion.Stable;
+ existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
+ ? bool.Parse(enableNextVideoInfoOverlay)
+ : true;
+ existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
+ ? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
+ : 10000;
+ existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
+ ? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
+ : 30000;
+ existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
+ ? theme
+ : string.Empty;
+ existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
+ ? home
+ : string.Empty;
+ 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));
+ if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
+ {
+ type = order < 7 ? defaults[order] : HomeSectionType.None;
+ }
+
+ existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
+ }
+
+ foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
+ {
+ var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
+ itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
+ _displayPreferencesManager.SaveChanges(itemPreferences);
+ }
+
+ var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
+ itemPrefs.SortBy = displayPreferences.SortBy;
+ itemPrefs.SortOrder = displayPreferences.SortOrder;
+ itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
+ itemPrefs.RememberSorting = displayPreferences.RememberSorting;
+
+ if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
+ {
+ itemPrefs.ViewType = viewType;
+ }
+
+ _displayPreferencesManager.SaveChanges(existingDisplayPreferences);
+ _displayPreferencesManager.SaveChanges(itemPrefs);
+
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
new file mode 100644
index 000000000..397299a73
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -0,0 +1,132 @@
+using System.Collections.Generic;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Dlna;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Dlna Controller.
+ /// </summary>
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public class DlnaController : BaseJellyfinApiController
+ {
+ private readonly IDlnaManager _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>
+ /// 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 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] string profileId)
+ {
+ var profile = _dlnaManager.GetProfile(profileId);
+ if (profile == null)
+ {
+ return NotFound();
+ }
+
+ 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] string profileId)
+ {
+ var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
+ if (existingDeviceProfile == null)
+ {
+ return NotFound();
+ }
+
+ _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>
+ /// 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] string profileId, [FromBody] DeviceProfile deviceProfile)
+ {
+ var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
+ if (existingDeviceProfile == null)
+ {
+ return NotFound();
+ }
+
+ _dlnaManager.UpdateProfile(deviceProfile);
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
new file mode 100644
index 000000000..0100d642b
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading.Tasks;
+using Emby.Dlna;
+using Emby.Dlna.Main;
+using Jellyfin.Api.Attributes;
+using MediaBrowser.Controller.Dlna;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Dlna Server Controller.
+ /// </summary>
+ [Route("Dlna")]
+ public class DlnaServerController : BaseJellyfinApiController
+ {
+ private const string XMLContentType = "text/xml; charset=UTF-8";
+
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IContentDirectory _contentDirectory;
+ private readonly IConnectionManager _connectionManager;
+ private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
+
+ /// <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>
+ /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
+ [HttpGet("{serverId}/description")]
+ [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
+ [Produces(XMLContentType)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetDescriptionXml([FromRoute] 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>
+ /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
+ [HttpGet("{serverId}/ContentDirectory")]
+ [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")]
+ [Produces(XMLContentType)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ public ActionResult GetContentDirectory([FromRoute] string serverId)
+ {
+ return Ok(_contentDirectory.GetServiceXml());
+ }
+
+ /// <summary>
+ /// Gets Dlna media receiver registrar xml.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <returns>Dlna media receiver registrar xml.</returns>
+ [HttpGet("{serverId}/MediaReceiverRegistrar")]
+ [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
+ [Produces(XMLContentType)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
+ {
+ return Ok(_mediaReceiverRegistrar.GetServiceXml());
+ }
+
+ /// <summary>
+ /// Gets Dlna media receiver registrar xml.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <returns>Dlna media receiver registrar xml.</returns>
+ [HttpGet("{serverId}/ConnectionManager")]
+ [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")]
+ [Produces(XMLContentType)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ public ActionResult GetConnectionManager([FromRoute] string serverId)
+ {
+ return Ok(_connectionManager.GetServiceXml());
+ }
+
+ /// <summary>
+ /// Process a content directory control request.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <returns>Control response.</returns>
+ [HttpPost("{serverId}/ContentDirectory/Control")]
+ public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] 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>
+ /// <returns>Control response.</returns>
+ [HttpPost("{serverId}/ConnectionManager/Control")]
+ public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] 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>
+ /// <returns>Control response.</returns>
+ [HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
+ public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string serverId)
+ {
+ return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Processes an event subscription request.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <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")]
+ public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
+ {
+ return ProcessEventRequest(_mediaReceiverRegistrar);
+ }
+
+ /// <summary>
+ /// Processes an event subscription request.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <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")]
+ public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
+ {
+ return ProcessEventRequest(_contentDirectory);
+ }
+
+ /// <summary>
+ /// Processes an event subscription request.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <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")]
+ 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>
+ /// <returns>Icon stream.</returns>
+ [HttpGet("{serverId}/icons/{fileName}")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
+ {
+ return GetIconInternal(fileName);
+ }
+
+ /// <summary>
+ /// Gets a server icon.
+ /// </summary>
+ /// <param name="fileName">The icon filename.</param>
+ /// <returns>Icon stream.</returns>
+ [HttpGet("icons/{fileName}")]
+ public ActionResult GetIcon([FromRoute] string fileName)
+ {
+ return GetIconInternal(fileName);
+ }
+
+ private ActionResult GetIconInternal(string fileName)
+ {
+ var icon = _dlnaManager.GetIcon(fileName);
+ if (icon == null)
+ {
+ return NotFound();
+ }
+
+ var contentType = "image/" + Path.GetExtension(fileName)
+ .TrimStart('.')
+ .ToLowerInvariant();
+
+ return File(icon.Stream, contentType);
+ }
+
+ private string GetAbsoluteUri()
+ {
+ return $"{Request.Scheme}://{Request.Host}{Request.Path}";
+ }
+
+ private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
+ {
+ return service.ProcessControlRequestAsync(new ControlRequest
+ {
+ Headers = Request.Headers,
+ InputXml = requestStream,
+ TargetServerUuId = id,
+ RequestedUrl = GetAbsoluteUri()
+ });
+ }
+
+ private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager)
+ {
+ var subscriptionId = Request.Headers["SID"];
+ if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
+ {
+ var notificationType = Request.Headers["NT"];
+ var callback = Request.Headers["CALLBACK"];
+ var timeoutString = Request.Headers["TIMEOUT"];
+
+ if (string.IsNullOrEmpty(notificationType))
+ {
+ return eventManager.RenewEventSubscription(
+ subscriptionId,
+ notificationType,
+ timeoutString,
+ callback);
+ }
+
+ return eventManager.CreateEventSubscription(notificationType, timeoutString, callback);
+ }
+
+ return eventManager.CancelEventSubscription(subscriptionId);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
new file mode 100644
index 000000000..b4fe3bc8f
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -0,0 +1,1789 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaybackDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Dynamic hls controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class DynamicHlsController : BaseJellyfinApiController
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IAuthorizationContext _authContext;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+ private readonly IConfiguration _configuration;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ILogger<DynamicHlsController> _logger;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly DynamicHlsHelper _dynamicHlsHelper;
+
+ private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
+
+ /// <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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
+ /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
+ public DynamicHlsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDlnaManager dlnaManager,
+ IAuthorizationContext authContext,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ ISubtitleEncoder subtitleEncoder,
+ IConfiguration configuration,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ ILogger<DynamicHlsController> logger,
+ DynamicHlsHelper dynamicHlsHelper)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dlnaManager = dlnaManager;
+ _authContext = authContext;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _subtitleEncoder = subtitleEncoder;
+ _configuration = configuration;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _logger = logger;
+ _dynamicHlsHelper = dynamicHlsHelper;
+
+ _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+ }
+
+ /// <summary>
+ /// Gets a video hls playlist 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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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)]
+ public async Task<ActionResult> GetMasterHlsVideoPlaylist(
+ [FromRoute] Guid itemId,
+ [FromRoute] 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, 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? 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ {
+ var streamingRequest = new HlsVideoRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ StreamOptions = streamOptions,
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets an audio hls playlist 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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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)]
+ public async Task<ActionResult> GetMasterHlsAudioPlaylist(
+ [FromRoute] Guid itemId,
+ [FromRoute] 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, 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? 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ {
+ var streamingRequest = new HlsAudioRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ StreamOptions = streamOptions,
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets a video stream using HTTP live streaming.
+ /// </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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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)]
+ public async Task<ActionResult> GetVariantHlsVideoPlaylist(
+ [FromRoute] Guid itemId,
+ [FromRoute] 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new VideoRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ StreamOptions = streamOptions
+ };
+
+ return await GetVariantPlaylistInternal(streamingRequest, "main", cancellationTokenSource)
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets an audio stream using HTTP live streaming.
+ /// </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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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)]
+ public async Task<ActionResult> GetVariantHlsAudioPlaylist(
+ [FromRoute] Guid itemId,
+ [FromRoute] 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new StreamingRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ StreamOptions = streamOptions
+ };
+
+ return await GetVariantPlaylistInternal(streamingRequest, "main", 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="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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetHlsVideoSegment(
+ [FromRoute] Guid itemId,
+ [FromRoute] string playlistId,
+ [FromRoute] int segmentId,
+ [FromRoute] 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var streamingRequest = new VideoRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ 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="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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetHlsAudioSegment(
+ [FromRoute] Guid itemId,
+ [FromRoute] string playlistId,
+ [FromRoute] int segmentId,
+ [FromRoute] 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var streamingRequest = new StreamingRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ StreamOptions = streamOptions
+ };
+
+ return await GetDynamicSegment(streamingRequest, segmentId)
+ .ConfigureAwait(false);
+ }
+
+ private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
+ {
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ Request,
+ _authContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _fileSystem,
+ _subtitleEncoder,
+ _configuration,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ _transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ Response.Headers.Add(HeaderNames.Expires, "0");
+
+ var segmentLengths = GetSegmentLengths(state);
+
+ var builder = new StringBuilder();
+
+ builder.AppendLine("#EXTM3U");
+ builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+ builder.AppendLine("#EXT-X-VERSION:3");
+ builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
+ builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+
+ var queryString = Request.QueryString;
+ var index = 0;
+
+ var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
+
+ foreach (var length in segmentLengths)
+ {
+ builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
+ builder.AppendLine(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "hls1/{0}/{1}{2}{3}",
+ name,
+ index.ToString(CultureInfo.InvariantCulture),
+ segmentExtension,
+ queryString));
+
+ index++;
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
+
+ private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId)
+ {
+ if ((streamingRequest.StartTimeTicks ?? 0) > 0)
+ {
+ throw new ArgumentException("StartTimeTicks is not allowed.");
+ }
+
+ var cancellationTokenSource = new CancellationTokenSource();
+ var cancellationToken = cancellationTokenSource.Token;
+
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ Request,
+ _authContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _fileSystem,
+ _subtitleEncoder,
+ _configuration,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ _transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
+
+ var segmentPath = GetSegmentPath(state, playlistPath, segmentId);
+
+ var segmentExtension = GetSegmentFileExtension(state.Request.SegmentContainer);
+
+ TranscodingJobDto? job;
+
+ 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);
+ }
+
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ var released = false;
+ var startTranscoding = false;
+
+ try
+ {
+ if (System.IO.File.Exists(segmentPath))
+ {
+ 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);
+ }
+ else
+ {
+ var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+ var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
+
+ if (currentTranscodingIndex == 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;
+ }
+
+ if (startTranscoding)
+ {
+ // 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 = GetStartPositionTicks(state, segmentId);
+
+ state.WaitForPath = segmentPath;
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ job = await _transcodingJobHelper.StartFfMpeg(
+ state,
+ playlistPath,
+ GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId),
+ Request,
+ _transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+ catch
+ {
+ state.Dispose();
+ throw;
+ }
+
+ // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ else
+ {
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ if (job?.TranscodingThrottler != null)
+ {
+ await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
+ }
+ }
+ }
+ }
+ finally
+ {
+ if (!released)
+ {
+ transcodingLock.Release();
+ }
+ }
+
+ _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 double[] GetSegmentLengths(StreamState state)
+ {
+ var result = new List<double>();
+
+ var ticks = state.RunTimeTicks ?? 0;
+
+ var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
+
+ while (ticks > 0)
+ {
+ var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
+
+ result.Add(TimeSpan.FromTicks(length).TotalSeconds);
+
+ ticks -= length;
+ }
+
+ return result.ToArray();
+ }
+
+ private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber)
+ {
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+
+ var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
+
+ 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 inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
+
+ // If isEncoding is true we're actually starting ffmpeg
+ var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
+
+ var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
+
+ var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
+
+ var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+ if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ segmentFormat = "mpegts";
+ }
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
+ inputModifier,
+ _encodingHelper.GetInputArgument(state, encodingOptions),
+ threads,
+ mapArgs,
+ GetVideoArguments(state, encodingOptions, startNumber),
+ GetAudioArguments(state, encodingOptions),
+ state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+ segmentFormat,
+ startNumberParam,
+ outputTsArg,
+ outputPath).Trim();
+ }
+
+ private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
+ {
+ var audioCodec = _encodingHelper.GetAudioEncoder(state);
+
+ if (!state.IsOutputVideo)
+ {
+ if (EncodingHelper.IsCopyCodec(audioCodec))
+ {
+ return "-acodec copy";
+ }
+
+ var audioTranscodeParams = new List<string>();
+
+ audioTranscodeParams.Add("-acodec " + audioCodec);
+
+ if (state.OutputAudioBitrate.HasValue)
+ {
+ audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (state.OutputAudioChannels.HasValue)
+ {
+ audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ audioTranscodeParams.Add("-vn");
+ return string.Join(' ', audioTranscodeParams);
+ }
+
+ if (EncodingHelper.IsCopyCodec(audioCodec))
+ {
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+
+ if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
+ {
+ return "-codec:a:0 copy -copypriorss:a:0 0";
+ }
+
+ return "-codec:a:0 copy";
+ }
+
+ var args = "-codec:a:0 " + audioCodec;
+
+ var channels = state.OutputAudioChannels;
+
+ if (channels.HasValue)
+ {
+ args += " -ac " + channels.Value;
+ }
+
+ var bitrate = state.OutputAudioBitrate;
+
+ if (bitrate.HasValue)
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true);
+
+ return args;
+ }
+
+ private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber)
+ {
+ if (!state.IsOutputVideo)
+ {
+ return string.Empty;
+ }
+
+ var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+
+ var args = "-codec:v:0 " + codec;
+
+ // if (state.EnableMpegtsM2TsMode)
+ // {
+ // args += " -mpegts_m2ts_mode 1";
+ // }
+
+ // See if we can save come cpu cycles by avoiding encoding
+ if (EncodingHelper.IsCopyCodec(codec))
+ {
+ if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+ if (!string.IsNullOrEmpty(bitStreamArgs))
+ {
+ args += " " + bitStreamArgs;
+ }
+ }
+
+ // args += " -flags -global_header";
+ }
+ else
+ {
+ var gopArg = string.Empty;
+ var keyFrameArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
+ startNumber * state.SegmentLength,
+ state.SegmentLength);
+
+ var framerate = state.VideoStream?.RealFrameRate;
+
+ if (framerate.HasValue)
+ {
+ // This is to make sure keyframe interval is limited to our segment,
+ // as forcing keyframes is not enough.
+ // Example: we encoded half of desired length, then codec detected
+ // scene cut and inserted a keyframe; next forced keyframe would
+ // be created outside of segment, which breaks seeking
+ // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
+ gopArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -g {0} -keyint_min {0} -sc_threshold 0",
+ Math.Ceiling(state.SegmentLength * framerate.Value));
+ }
+
+ args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
+
+ // Unable to force key frames using these hw encoders, set key frames by GOP
+ if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " " + gopArg;
+ }
+ else
+ {
+ args += " " + keyFrameArg + gopArg;
+ }
+
+ // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
+
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
+ // This is for graphical subs
+ if (hasGraphicalSubs)
+ {
+ args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
+ }
+
+ // Add resolution params, if specified
+ else
+ {
+ args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
+ }
+
+ // -start_at_zero is necessary to use with -ss when seeking,
+ // otherwise the target position cannot be determined.
+ if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
+ {
+ args += " -start_at_zero";
+ }
+
+ // args += " -flags -global_header";
+ }
+
+ if (!string.IsNullOrEmpty(state.OutputVideoSync))
+ {
+ args += " -vsync " + state.OutputVideoSync;
+ }
+
+ args += _encodingHelper.GetOutputFFlags(state);
+
+ return args;
+ }
+
+ private string GetSegmentFileExtension(string? segmentContainer)
+ {
+ if (!string.IsNullOrWhiteSpace(segmentContainer))
+ {
+ return "." + segmentContainer;
+ }
+
+ return ".ts";
+ }
+
+ private string GetSegmentPath(StreamState state, string playlist, int index)
+ {
+ var folder = Path.GetDirectoryName(playlist);
+
+ var filename = Path.GetFileNameWithoutExtension(playlist);
+
+ return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));
+ }
+
+ 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)
+ {
+ if (transcodingJob != 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, segmentIndex, transcodingJob);
+ }
+
+ 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, segmentIndex, transcodingJob);
+ }
+ }
+
+ var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
+ if (transcodingJob != null)
+ {
+ 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)
+ {
+ if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath))
+ {
+ _logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
+ return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+ }
+ }
+ else
+ {
+ segmentExists = System.IO.File.Exists(segmentPath);
+ if (segmentExists)
+ {
+ continue; // avoid unnecessary waiting if segment just became available
+ }
+ }
+
+ 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);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+ else
+ {
+ _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
+ }
+
+ return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+ }
+
+ private ActionResult GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJobDto? transcodingJob)
+ {
+ var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
+
+ Response.OnCompleted(() =>
+ {
+ _logger.LogDebug("finished serving {0}", segmentPath);
+ if (transcodingJob != null)
+ {
+ transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
+ _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+ }
+
+ return Task.CompletedTask;
+ });
+
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
+ }
+
+ private long GetEndPositionTicks(StreamState state, int requestedIndex)
+ {
+ double startSeconds = 0;
+ var lengths = GetSegmentLengths(state);
+
+ if (requestedIndex >= lengths.Length)
+ {
+ var msg = string.Format(
+ CultureInfo.InvariantCulture,
+ "Invalid segment index requested: {0} - Segment count: {1}",
+ requestedIndex,
+ lengths.Length);
+ throw new ArgumentException(msg);
+ }
+
+ for (var i = 0; i <= requestedIndex; i++)
+ {
+ startSeconds += lengths[i];
+ }
+
+ return TimeSpan.FromSeconds(startSeconds).Ticks;
+ }
+
+ private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
+ {
+ var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType);
+
+ if (job == null || job.HasExited)
+ {
+ return null;
+ }
+
+ var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem);
+
+ if (file == null)
+ {
+ return null;
+ }
+
+ var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+
+ 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);
+
+ var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
+
+ 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;
+ }
+ }
+
+ private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ {
+ var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
+
+ if (file != null)
+ {
+ DeleteFile(file.FullName, retryCount);
+ }
+ }
+
+ private void DeleteFile(string path, int retryCount)
+ {
+ if (retryCount >= 5)
+ {
+ return;
+ }
+
+ _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);
+
+ var task = Task.Delay(100);
+ Task.WaitAll(task);
+ DeleteFile(path, retryCount + 1);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
+ }
+ }
+
+ private long GetStartPositionTicks(StreamState state, int requestedIndex)
+ {
+ double startSeconds = 0;
+ var lengths = GetSegmentLengths(state);
+
+ if (requestedIndex >= lengths.Length)
+ {
+ var msg = string.Format(
+ CultureInfo.InvariantCulture,
+ "Invalid segment index requested: {0} - Segment count: {1}",
+ requestedIndex,
+ lengths.Length);
+ throw new ArgumentException(msg);
+ }
+
+ for (var i = 0; i < requestedIndex; i++)
+ {
+ startSeconds += lengths[i];
+ }
+
+ var position = TimeSpan.FromSeconds(startSeconds).Ticks;
+ return position;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
new file mode 100644
index 000000000..64670f7d8
--- /dev/null
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.EnvironmentDtos;
+using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Environment Controller.
+ /// </summary>
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public class EnvironmentController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <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)
+ {
+ return Array.Empty<FileSystemEntryInfo>();
+ }
+
+ 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));
+ }
+
+ /// <summary>
+ /// Validates path.
+ /// </summary>
+ /// <param name="validatePathDto">Validate request object.</param>
+ /// <response code="200">Path validated.</response>
+ /// <response code="404">Path not found.</response>
+ /// <returns>Validation status.</returns>
+ [HttpPost("ValidatePath")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
+ {
+ if (validatePathDto.IsFile.HasValue)
+ {
+ if (validatePathDto.IsFile.Value)
+ {
+ if (!System.IO.File.Exists(validatePathDto.Path))
+ {
+ return NotFound();
+ }
+ }
+ else
+ {
+ if (!Directory.Exists(validatePathDto.Path))
+ {
+ return NotFound();
+ }
+ }
+ }
+ else
+ {
+ if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
+ {
+ return NotFound();
+ }
+
+ if (validatePathDto.ValidateWritable)
+ {
+ var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
+ try
+ {
+ System.IO.File.WriteAllText(file, string.Empty);
+ }
+ finally
+ {
+ if (System.IO.File.Exists(file))
+ {
+ System.IO.File.Delete(file);
+ }
+ }
+ }
+ }
+
+ return Ok();
+ }
+
+ /// <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 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))
+ {
+ // Check if unc share
+ var index = path.LastIndexOf(UncSeparator);
+
+ if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ parent = path.Substring(0, index);
+
+ if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
+ {
+ parent = null;
+ }
+ }
+ }
+
+ return parent;
+ }
+
+ /// <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
new file mode 100644
index 000000000..2a567c846
--- /dev/null
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Filters controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class FilterController : BaseJellyfinApiController
+ {
+ 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] string? parentId,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? mediaTypes)
+ {
+ var parentItem = string.IsNullOrEmpty(parentId)
+ ? null
+ : _libraryManager.GetItemById(parentId);
+
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ {
+ parentItem = null;
+ }
+
+ var item = string.IsNullOrEmpty(parentId)
+ ? user == null
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder()
+ : parentItem;
+
+ var query = new InternalItemsQuery
+ {
+ User = user,
+ MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ Recursive = true,
+ EnableTotalRecordCount = false,
+ DtoOptions = new DtoOptions
+ {
+ Fields = new[] { ItemFields.Genres, ItemFields.Tags },
+ EnableImages = false,
+ EnableUserData = false
+ }
+ };
+
+ var itemList = ((Folder)item!).GetItemList(query);
+ return new QueryFiltersLegacy
+ {
+ Years = itemList.Select(i => i.ProductionYear ?? -1)
+ .Where(i => i > 0)
+ .Distinct()
+ .OrderBy(i => i)
+ .ToArray(),
+
+ Genres = itemList.SelectMany(i => i.Genres)
+ .DistinctNames()
+ .OrderBy(i => i)
+ .ToArray(),
+
+ Tags = itemList
+ .SelectMany(i => i.Tags)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(i => i)
+ .ToArray(),
+
+ OfficialRatings = itemList
+ .Select(i => i.OfficialRating)
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(i => i)
+ .ToArray()
+ };
+ }
+
+ /// <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] string? parentId,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] bool? isAiring,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? recursive)
+ {
+ var parentItem = string.IsNullOrEmpty(parentId)
+ ? null
+ : _libraryManager.GetItemById(parentId);
+
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ {
+ parentItem = null;
+ }
+
+ var filters = new QueryFilters();
+ var genreQuery = new InternalItemsQuery(user)
+ {
+ IncludeItemTypes =
+ (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ DtoOptions = new DtoOptions
+ {
+ Fields = Array.Empty<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 == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
+ }
+ else
+ {
+ genreQuery.Parent = parentItem;
+ }
+
+ if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
+ {
+ filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
+ {
+ Name = i.Item1.Name,
+ Id = i.Item1.Id
+ }).ToArray();
+ }
+ else
+ {
+ filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
+ {
+ Name = i.Item1.Name,
+ Id = i.Item1.Id
+ }).ToArray();
+ }
+
+ return filters;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
new file mode 100644
index 000000000..55ad71200
--- /dev/null
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -0,0 +1,320 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The genres controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class GenresController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <summary>
+ /// Gets all genres 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">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="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="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="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 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="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="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] double? minCommunityRating,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? genres,
+ [FromQuery] string? genreIds,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? studioIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem;
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ user = _userManager.GetUserById(userId.Value);
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+ }
+ else
+ {
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+ }
+
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
+ GenreIds = RequestHelpers.GetGuids(genreIds),
+ StudioIds = RequestHelpers.GetGuids(studioIds),
+ Person = person,
+ PersonIds = RequestHelpers.GetGuids(personIds),
+ PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+ Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ if (parentItem is Folder)
+ {
+ query.AncestorIds = new[] { new Guid(parentId) };
+ }
+ else
+ {
+ query.ItemIds = new[] { new Guid(parentId) };
+ }
+ }
+
+ // Studios
+ if (!string.IsNullOrEmpty(studios))
+ {
+ query.StudioIds = studios.Split('|')
+ .Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(i);
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != null)
+ .Select(i => i!.Id)
+ .ToArray();
+ }
+
+ foreach (var filter in RequestHelpers.GetFilters(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 = new QueryResult<(BaseItem, ItemCounts)>();
+
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, counts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (!string.IsNullOrWhiteSpace(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>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
+
+ /// <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] string genreName, [FromQuery] Guid? userId)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(Request);
+
+ Genre item = new Genre();
+ if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions);
+
+ if (result != null)
+ {
+ item = result;
+ }
+ }
+ else
+ {
+ item = _libraryManager.GetGenre(genreName);
+ }
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
+ }
+
+ private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+ where T : BaseItem, new()
+ {
+ var result = libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '&'),
+ IncludeItemTypes = new[] { typeof(T).Name },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
+
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '/'),
+ IncludeItemTypes = new[] { typeof(T).Name },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
+
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '?'),
+ IncludeItemTypes = new[] { typeof(T).Name },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
+
+ return result;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
new file mode 100644
index 000000000..816252f80
--- /dev/null
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+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>
+ /// 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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+ public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
+ {
+ // TODO: Deprecate with new iOS app
+ var file = segmentId + Path.GetExtension(Request.Path);
+ file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
+ }
+
+ /// <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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+ public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId)
+ {
+ var file = playlistId + Path.GetExtension(Request.Path);
+ file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), 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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] 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>
+ /// <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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+ public ActionResult GetHlsVideoSegmentLegacy(
+ [FromRoute] string itemId,
+ [FromRoute] string playlistId,
+ [FromRoute] string segmentId,
+ [FromRoute] string segmentContainer)
+ {
+ var file = segmentId + Path.GetExtension(Request.Path);
+ var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
+
+ file = Path.Combine(transcodeFolderPath, file);
+
+ var normalizedPlaylistId = playlistId;
+
+ var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
+ .FirstOrDefault(i =>
+ string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
+ && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
+
+ return GetFileResult(file, playlistPath);
+ }
+
+ private ActionResult GetFileResult(string path, string playlistPath)
+ {
+ var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+
+ Response.OnCompleted(() =>
+ {
+ if (transcodingJob != null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+ }
+
+ return Task.CompletedTask;
+ });
+
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
new file mode 100644
index 000000000..528590536
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Images By Name Controller.
+ /// </summary>
+ [Route("Images")]
+ public class ImageByNameController : BaseJellyfinApiController
+ {
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ImageByNameController" /> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param>
+ public ImageByNameController(
+ IServerConfigurationManager serverConfigurationManager,
+ IFileSystem fileSystem)
+ {
+ _applicationPaths = serverConfigurationManager.ApplicationPaths;
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
+ /// Get all general images.
+ /// </summary>
+ /// <response code="200">Retrieved list of images.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
+ [HttpGet("General")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
+ {
+ return GetImageList(_applicationPaths.GeneralPath, false);
+ }
+
+ /// <summary>
+ /// Get General Image.
+ /// </summary>
+ /// <param name="name">The name of the image.</param>
+ /// <param name="type">Image Type (primary, backdrop, logo, etc).</param>
+ /// <response code="200">Image stream retrieved.</response>
+ /// <response code="404">Image not found.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
+ [HttpGet("General/{name}/{type}")]
+ [AllowAnonymous]
+ [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<FileStreamResult> GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type)
+ {
+ var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
+ ? "folder"
+ : type;
+
+ var path = BaseItem.SupportedImageExtensions
+ .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
+ .FirstOrDefault(System.IO.File.Exists);
+
+ if (path == null)
+ {
+ return NotFound();
+ }
+
+ var contentType = MimeTypes.GetMimeType(path);
+ return File(System.IO.File.OpenRead(path), contentType);
+ }
+
+ /// <summary>
+ /// Get all general images.
+ /// </summary>
+ /// <response code="200">Retrieved list of images.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
+ [HttpGet("Ratings")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
+ {
+ return GetImageList(_applicationPaths.RatingsPath, false);
+ }
+
+ /// <summary>
+ /// Get rating image.
+ /// </summary>
+ /// <param name="theme">The theme to get the image from.</param>
+ /// <param name="name">The name of the image.</param>
+ /// <response code="200">Image stream retrieved.</response>
+ /// <response code="404">Image not found.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
+ [HttpGet("Ratings/{theme}/{name}")]
+ [AllowAnonymous]
+ [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<FileStreamResult> GetRatingImage(
+ [FromRoute, Required] string? theme,
+ [FromRoute, Required] string? name)
+ {
+ return GetImageFile(_applicationPaths.RatingsPath, theme, name);
+ }
+
+ /// <summary>
+ /// Get all media info images.
+ /// </summary>
+ /// <response code="200">Image list retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
+ [HttpGet("MediaInfo")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
+ {
+ return GetImageList(_applicationPaths.MediaInfoImagesPath, false);
+ }
+
+ /// <summary>
+ /// Get media info image.
+ /// </summary>
+ /// <param name="theme">The theme to get the image from.</param>
+ /// <param name="name">The name of the image.</param>
+ /// <response code="200">Image stream retrieved.</response>
+ /// <response code="404">Image not found.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
+ [HttpGet("MediaInfo/{theme}/{name}")]
+ [AllowAnonymous]
+ [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<FileStreamResult> GetMediaInfoImage(
+ [FromRoute, Required] string? theme,
+ [FromRoute, Required] string? name)
+ {
+ return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
+ }
+
+ /// <summary>
+ /// Internal FileHelper.
+ /// </summary>
+ /// <param name="basePath">Path to begin search.</param>
+ /// <param name="theme">Theme to search.</param>
+ /// <param name="name">File name to search for.</param>
+ /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
+ private ActionResult<FileStreamResult> GetImageFile(string basePath, string? theme, string? name)
+ {
+ var themeFolder = Path.Combine(basePath, theme);
+ if (Directory.Exists(themeFolder))
+ {
+ var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
+ .FirstOrDefault(System.IO.File.Exists);
+
+ if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
+ {
+ var contentType = MimeTypes.GetMimeType(path);
+ return File(System.IO.File.OpenRead(path), contentType);
+ }
+ }
+
+ var allFolder = Path.Combine(basePath, "all");
+ if (Directory.Exists(allFolder))
+ {
+ var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
+ .FirstOrDefault(System.IO.File.Exists);
+
+ if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
+ {
+ var contentType = MimeTypes.GetMimeType(path);
+ return File(System.IO.File.OpenRead(path), contentType);
+ }
+ }
+
+ return NotFound();
+ }
+
+ private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
+ {
+ try
+ {
+ return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
+ .Select(i => new ImageByNameInfo
+ {
+ Name = _fileSystem.GetFileNameWithoutExtension(i),
+ FileLength = i.Length,
+
+ // For themeable images, use the Theme property
+ // For general images, the same object structure is fine,
+ // but it's not owned by a theme, so call it Context
+ Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
+ Context = supportsThemes ? null : GetThemeName(i.FullName, path),
+ Format = i.Extension.ToLowerInvariant().TrimStart('.')
+ })
+ .OrderBy(i => i.Name)
+ .ToList();
+ }
+ catch (IOException)
+ {
+ return new List<ImageByNameInfo>();
+ }
+ }
+
+ private string? GetThemeName(string path, string rootImagePath)
+ {
+ var parentName = Path.GetDirectoryName(path);
+
+ if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ parentName = Path.GetFileName(parentName);
+
+ return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
new file mode 100644
index 000000000..75734f0af
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -0,0 +1,1304 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+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 IAuthorizationContext _authContext;
+ private readonly ILogger<ImageController> _logger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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>
+ public ImageController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ IImageProcessor imageProcessor,
+ IFileSystem fileSystem,
+ IAuthorizationContext authContext,
+ ILogger<ImageController> logger,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _imageProcessor = imageProcessor;
+ _fileSystem = fileSystem;
+ _authContext = authContext;
+ _logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <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}")]
+ [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [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] Guid userId,
+ [FromRoute] ImageType imageType,
+ [FromRoute] int? index = null)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return Forbid("User is not allowed to update the image.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+ await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage != null)
+ {
+ _userManager.ClearProfileImage(user);
+ }
+
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+
+ 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/{itemType}")]
+ [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
+ [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 ActionResult DeleteUserImage(
+ [FromRoute] Guid userId,
+ [FromRoute] ImageType imageType,
+ [FromRoute] int? index = null)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return Forbid("User is not allowed to delete the image.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+ try
+ {
+ System.IO.File.Delete(user.ProfileImage.Path);
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error deleting user profile image:");
+ }
+
+ _userManager.ClearProfileImage(user);
+ return NoContent();
+ }
+
+ /// <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}")]
+ [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult DeleteItemImage(
+ [FromRoute] Guid itemId,
+ [FromRoute] ImageType imageType,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ item.DeleteImage(imageType, imageIndex ?? 0);
+ return NoContent();
+ }
+
+ /// <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}")]
+ [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> SetItemImage(
+ [FromRoute] Guid itemId,
+ [FromRoute] ImageType imageType,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+
+ return NoContent();
+ }
+
+ /// <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 ActionResult UpdateItemImageIndex(
+ [FromRoute] Guid itemId,
+ [FromRoute] ImageType imageType,
+ [FromRoute] int imageIndex,
+ [FromQuery] int newIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ item.SwapImages(imageType, imageIndex, newIndex);
+ return NoContent();
+ }
+
+ /// <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 ActionResult<IEnumerable<ImageInfo>> GetItemImageInfos([FromRoute] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ var list = new List<ImageInfo>();
+ var itemImages = item.ImageInfos;
+
+ if (itemImages.Length == 0)
+ {
+ // short-circuit
+ return list;
+ }
+
+ _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct
+
+ foreach (var image in itemImages)
+ {
+ if (!item.AllowsMultipleImages(image.Type))
+ {
+ var info = GetImageInfo(item, image, null);
+
+ if (info != null)
+ {
+ list.Add(info);
+ }
+ }
+ }
+
+ foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
+ {
+ var index = 0;
+
+ // Prevent implicitly captured closure
+ var currentImageType = imageType;
+
+ foreach (var image in itemImages.Where(i => i.Type == currentImageType))
+ {
+ var info = GetImageInfo(item, image, index);
+
+ if (info != null)
+ {
+ list.Add(info);
+ }
+
+ index++;
+ }
+ }
+
+ return list;
+ }
+
+ /// <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="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}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
+ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
+ [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetItemImage(
+ [FromRoute] Guid itemId,
+ [FromRoute] ImageType imageType,
+ [FromRoute] int? maxWidth,
+ [FromRoute] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] string? tag,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] string? format,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .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="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)]
+ public async Task<ActionResult> GetItemImage2(
+ [FromRoute] Guid itemId,
+ [FromRoute] ImageType imageType,
+ [FromRoute] int? maxWidth,
+ [FromRoute] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromRoute] string tag,
+ [FromQuery] bool? cropWhitespace,
+ [FromRoute] string format,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromRoute] double? percentPlayed,
+ [FromRoute] int? unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .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="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)]
+ public async Task<ActionResult> GetArtistImage(
+ [FromRoute] string name,
+ [FromRoute] ImageType imageType,
+ [FromRoute] string tag,
+ [FromRoute] string format,
+ [FromRoute] int? maxWidth,
+ [FromRoute] int? maxHeight,
+ [FromRoute] double? percentPlayed,
+ [FromRoute] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetArtist(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .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="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}/{imageIndex?}")]
+ [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetGenreImage(
+ [FromRoute] string name,
+ [FromRoute] ImageType imageType,
+ [FromRoute] string tag,
+ [FromRoute] string format,
+ [FromRoute] int? maxWidth,
+ [FromRoute] int? maxHeight,
+ [FromRoute] double? percentPlayed,
+ [FromRoute] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetGenre(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .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="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}/{imageIndex?}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetMusicGenreImage(
+ [FromRoute] string name,
+ [FromRoute] ImageType imageType,
+ [FromRoute] string tag,
+ [FromRoute] string format,
+ [FromRoute] int? maxWidth,
+ [FromRoute] int? maxHeight,
+ [FromRoute] double? percentPlayed,
+ [FromRoute] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .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="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}/{imageIndex?}")]
+ [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetPersonImage(
+ [FromRoute] string name,
+ [FromRoute] ImageType imageType,
+ [FromRoute] string tag,
+ [FromRoute] string format,
+ [FromRoute] int? maxWidth,
+ [FromRoute] int? maxHeight,
+ [FromRoute] double? percentPlayed,
+ [FromRoute] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetPerson(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .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="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}/{imageIndex?}")]
+ [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetStudioImage(
+ [FromRoute] string name,
+ [FromRoute] ImageType imageType,
+ [FromRoute] string tag,
+ [FromRoute] string format,
+ [FromRoute] int? maxWidth,
+ [FromRoute] int? maxHeight,
+ [FromRoute] double? percentPlayed,
+ [FromRoute] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var item = _libraryManager.GetStudio(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <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="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}/{imageIndex?}")]
+ [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetUserImage(
+ [FromRoute] Guid userId,
+ [FromRoute] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] string? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute] int? imageIndex = null)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user == null)
+ {
+ return NotFound();
+ }
+
+ var info = new ItemImageInfo
+ {
+ Path = user.ProfileImage.Path,
+ Type = ImageType.Profile,
+ DateModified = user.ProfileImage.LastModified
+ };
+
+ if (width.HasValue)
+ {
+ info.Width = width.Value;
+ }
+
+ if (height.HasValue)
+ {
+ info.Height = height.Value;
+ }
+
+ return await GetImageInternal(
+ user.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ null,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
+ info)
+ .ConfigureAwait(false);
+ }
+
+ private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
+ {
+ using var reader = new StreamReader(inputStream);
+ var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+ var bytes = Convert.FromBase64String(text);
+ return new MemoryStream(bytes, 0, bytes.Length, false, true);
+ }
+
+ private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
+ {
+ int? width = null;
+ int? height = null;
+ string? blurhash = null;
+ long length = 0;
+
+ try
+ {
+ if (info.IsLocalFile)
+ {
+ var fileInfo = _fileSystem.GetFileInfo(info.Path);
+ length = fileInfo.Length;
+
+ blurhash = info.BlurHash;
+ width = info.Width;
+ height = info.Height;
+
+ if (width <= 0 || height <= 0)
+ {
+ width = null;
+ height = null;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
+ }
+
+ try
+ {
+ return new ImageInfo
+ {
+ Path = info.Path,
+ ImageIndex = imageIndex,
+ ImageType = info.Type,
+ ImageTag = _imageProcessor.GetImageCacheTag(item, info),
+ Size = length,
+ BlurHash = blurhash,
+ Width = width,
+ Height = height
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
+ return null;
+ }
+ }
+
+ private async Task<ActionResult> GetImageInternal(
+ Guid itemId,
+ ImageType imageType,
+ int? imageIndex,
+ string? tag,
+ string? format,
+ int? maxWidth,
+ int? maxHeight,
+ double? percentPlayed,
+ int? unplayedCount,
+ int? width,
+ int? height,
+ int? quality,
+ bool? cropWhitespace,
+ bool? addPlayedIndicator,
+ int? blur,
+ string? backgroundColor,
+ string? foregroundLayer,
+ BaseItem? item,
+ bool isHeadRequest,
+ ItemImageInfo? imageInfo = null)
+ {
+ if (percentPlayed.HasValue)
+ {
+ if (percentPlayed.Value <= 0)
+ {
+ percentPlayed = null;
+ }
+ else if (percentPlayed.Value >= 100)
+ {
+ percentPlayed = null;
+ addPlayedIndicator = true;
+ }
+ }
+
+ if (percentPlayed.HasValue)
+ {
+ unplayedCount = null;
+ }
+
+ if (unplayedCount.HasValue
+ && unplayedCount.Value <= 0)
+ {
+ unplayedCount = null;
+ }
+
+ if (imageInfo == null)
+ {
+ imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
+ if (imageInfo == null)
+ {
+ return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType));
+ }
+ }
+
+ cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art;
+
+ var outputFormats = GetOutputFormats(format);
+
+ TimeSpan? cacheDuration = null;
+
+ if (!string.IsNullOrEmpty(tag))
+ {
+ cacheDuration = TimeSpan.FromDays(365);
+ }
+
+ var responseHeaders = new Dictionary<string, string>
+ {
+ { "transferMode.dlna.org", "Interactive" },
+ { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
+ };
+
+ return await GetImageResult(
+ item,
+ itemId,
+ imageIndex,
+ height,
+ maxHeight,
+ maxWidth,
+ quality,
+ width,
+ addPlayedIndicator,
+ percentPlayed,
+ unplayedCount,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ imageInfo,
+ cropWhitespace.Value,
+ outputFormats,
+ cacheDuration,
+ responseHeaders,
+ isHeadRequest).ConfigureAwait(false);
+ }
+
+ private ImageFormat[] GetOutputFormats(string? format)
+ {
+ if (!string.IsNullOrWhiteSpace(format)
+ && Enum.TryParse(format, true, out ImageFormat parsedFormat))
+ {
+ return new[] { parsedFormat };
+ }
+
+ return GetClientSupportedFormats();
+ }
+
+ private ImageFormat[] GetClientSupportedFormats()
+ {
+ var acceptTypes = Request.Headers[HeaderNames.Accept];
+ var supportedFormats = new List<string>();
+ if (acceptTypes.Count > 0)
+ {
+ foreach (var type in acceptTypes)
+ {
+ int index = type.IndexOf(';', StringComparison.Ordinal);
+ if (index != -1)
+ {
+ supportedFormats.Add(type.Substring(0, index));
+ }
+ }
+ }
+
+ var acceptParam = Request.Query[HeaderNames.Accept];
+
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
+
+ if (!supportsWebP)
+ {
+ var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
+ if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 &&
+ userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ supportsWebP = true;
+ }
+ }
+
+ var formats = new List<ImageFormat>(4);
+
+ if (supportsWebP)
+ {
+ formats.Add(ImageFormat.Webp);
+ }
+
+ formats.Add(ImageFormat.Jpg);
+ formats.Add(ImageFormat.Png);
+
+ if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
+ {
+ formats.Add(ImageFormat.Gif);
+ }
+
+ return formats.ToArray();
+ }
+
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+ {
+ var mimeType = "image/" + format;
+
+ if (requestAcceptTypes.Contains(mimeType))
+ {
+ return true;
+ }
+
+ if (acceptAll && requestAcceptTypes.Contains("*/*"))
+ {
+ return true;
+ }
+
+ return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private async Task<ActionResult> GetImageResult(
+ BaseItem? item,
+ Guid itemId,
+ int? index,
+ int? height,
+ int? maxHeight,
+ int? maxWidth,
+ int? quality,
+ int? width,
+ bool? addPlayedIndicator,
+ double? percentPlayed,
+ int? unplayedCount,
+ int? blur,
+ string? backgroundColor,
+ string? foregroundLayer,
+ ItemImageInfo imageInfo,
+ bool cropWhitespace,
+ IReadOnlyCollection<ImageFormat> supportedFormats,
+ TimeSpan? cacheDuration,
+ IDictionary<string, string> headers,
+ bool isHeadRequest)
+ {
+ if (!imageInfo.IsLocalFile && item != null)
+ {
+ imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false);
+ }
+
+ var options = new ImageProcessingOptions
+ {
+ CropWhiteSpace = cropWhitespace,
+ Height = height,
+ ImageIndex = index ?? 0,
+ Image = imageInfo,
+ Item = item,
+ ItemId = itemId,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ Quality = quality ?? 100,
+ Width = width,
+ AddPlayedIndicator = addPlayedIndicator ?? false,
+ PercentPlayed = percentPlayed ?? 0,
+ UnplayedCount = unplayedCount,
+ Blur = blur,
+ BackgroundColor = backgroundColor,
+ ForegroundLayer = foregroundLayer,
+ SupportedOutputFormats = supportedFormats
+ };
+
+ var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(options).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;
+ }
+
+ foreach (var (key, value) in headers)
+ {
+ Response.Headers.Add(key, value);
+ }
+
+ Response.ContentType = imageContentType;
+ Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
+ Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
+
+ 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)
+ {
+ Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
+ }
+ else
+ {
+ Response.Headers.Add(HeaderNames.CacheControl, "public");
+ }
+
+ Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
+
+ // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
+ if (!(dateImageModified > ifModifiedSinceHeader))
+ {
+ if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
+ {
+ Response.StatusCode = StatusCodes.Status304NotModified;
+ return new ContentResult();
+ }
+ }
+ }
+
+ // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
+ if (isHeadRequest)
+ {
+ return NoContent();
+ }
+
+ var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read);
+ return File(stream, imageContentType);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
new file mode 100644
index 000000000..73bd30c4d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The instant mix controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class InstantMixController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
+ {
+ var item = _libraryManager.GetItemById(id);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
+ {
+ var album = _libraryManager.GetItemById(id);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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 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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
+ {
+ var playlist = (Playlist)_libraryManager.GetItemById(id);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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 song.
+ /// </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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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>> GetInstantMixFromMusicGenre(
+ [FromRoute, Required] string? name,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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 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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
+ [FromRoute] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
+ {
+ var item = _libraryManager.GetItemById(id);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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>> GetInstantMixFromMusicGenres(
+ [FromRoute] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
+ {
+ var item = _libraryManager.GetItemById(id);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
+ {
+ var item = _libraryManager.GetItemById(id);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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;
+
+ var result = new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = list.Count
+ };
+
+ if (limit.HasValue)
+ {
+ list = list.Take(limit.Value).ToList();
+ }
+
+ var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
+
+ result.Items = returnList;
+
+ return result;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
new file mode 100644
index 000000000..afde4a433
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -0,0 +1,364 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Item lookup controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class ItemLookupController : BaseJellyfinApiController
+ {
+ private readonly IProviderManager _providerManager;
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<ItemLookupController> _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="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> 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,
+ IServerConfigurationManager serverConfigurationManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ ILogger<ItemLookupController> logger)
+ {
+ _providerManager = providerManager;
+ _appPaths = serverConfigurationManager.ApplicationPaths;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ _logger = 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] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ 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 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 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 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 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>
+ /// Gets a remote image.
+ /// </summary>
+ /// <param name="imageUrl">The image url.</param>
+ /// <param name="providerName">The provider name.</param>
+ /// <response code="200">Remote image retrieved.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+ /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
+ /// </returns>
+ [HttpGet("Items/RemoteSearch/Image")]
+ public async Task<ActionResult> GetRemoteSearchImage(
+ [FromQuery, Required] string imageUrl,
+ [FromQuery, Required] string providerName)
+ {
+ var urlHash = imageUrl.GetMD5();
+ var pointerCachePath = GetFullCachePath(urlHash.ToString());
+
+ try
+ {
+ var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+ if (System.IO.File.Exists(contentPath))
+ {
+ await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath);
+ return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet);
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ // Means the file isn't cached yet
+ }
+ catch (IOException)
+ {
+ // Means the file isn't cached yet
+ }
+
+ await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
+
+ // Read the pointer file again
+ await using var fileStream = System.IO.File.OpenRead(pointerCachePath);
+ return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet);
+ }
+
+ /// <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/{id}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public async Task<ActionResult> ApplySearchCriteria(
+ [FromRoute] 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));
+
+ // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
+ item.ProviderIds = searchResult.ProviderIds;
+ await _providerManager.RefreshFullItem(
+ item,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllMetadata = true,
+ ReplaceAllImages = replaceAllImages,
+ SearchResult = searchResult
+ }, CancellationToken.None).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Downloads the image.
+ /// </summary>
+ /// <param name="providerName">Name of the provider.</param>
+ /// <param name="url">The URL.</param>
+ /// <param name="urlHash">The URL hash.</param>
+ /// <param name="pointerCachePath">The pointer cache path.</param>
+ /// <returns>Task.</returns>
+ private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
+ {
+ using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
+ var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
+ var fullCachePath = GetFullCachePath(urlHash + "." + ext);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+ using (var stream = result.Content)
+ {
+ await using var fileStream = new FileStream(
+ fullCachePath,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.Read,
+ IODefaults.FileStreamBufferSize,
+ true);
+
+ await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+ await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets the full cache path.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns>System.String.</returns>
+ private string GetFullCachePath(string filename)
+ => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
new file mode 100644
index 000000000..3f5d305c1
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -0,0 +1,85 @@
+using System;
+using System.ComponentModel;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Item Refresh Controller.
+ /// </summary>
+ [Route("Items")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class ItemRefreshController : BaseJellyfinApiController
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _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)
+ {
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _fileSystem = 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 Post(
+ [FromRoute] 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 == null)
+ {
+ return NotFound();
+ }
+
+ var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ MetadataRefreshMode = metadataRefreshMode,
+ ImageRefreshMode = imageRefreshMode,
+ ReplaceAllImages = replaceAllImages,
+ ReplaceAllMetadata = replaceAllMetadata,
+ ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
+ || imageRefreshMode == MetadataRefreshMode.FullRefresh
+ || replaceAllImages
+ || replaceAllMetadata,
+ IsAutomated = false
+ };
+
+ _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
+ return NoContent();
+ }
+ }
+}
diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 2db6d717a..4b40c6ada 100644
--- a/MediaBrowser.Api/ItemUpdateService.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -1,215 +1,100 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
+using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
-namespace MediaBrowser.Api
+namespace Jellyfin.Api.Controllers
{
- [Route("/Items/{ItemId}", "POST", Summary = "Updates an item")]
- public class UpdateItem : BaseItemDto, IReturnVoid
- {
- [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string ItemId { get; set; }
- }
-
- [Route("/Items/{ItemId}/MetadataEditor", "GET", Summary = "Gets metadata editor info for an item")]
- public class GetMetadataEditorInfo : IReturn<MetadataEditorInfo>
- {
- [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string ItemId { get; set; }
- }
-
- [Route("/Items/{ItemId}/ContentType", "POST", Summary = "Updates an item's content type")]
- public class UpdateItemContentType : IReturnVoid
- {
- [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid ItemId { get; set; }
-
- [ApiMember(Name = "ContentType", Description = "The content type of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string ContentType { get; set; }
- }
-
- [Authenticated(Roles = "admin")]
- public class ItemUpdateService : BaseApiService
+ /// <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;
-
- public ItemUpdateService(
- ILogger<ItemUpdateService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
+ 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)
- : base(logger, serverConfigurationManager, httpResultFactory)
+ ILocalizationManager localizationManager,
+ IServerConfigurationManager serverConfigurationManager)
{
_libraryManager = libraryManager;
_providerManager = providerManager;
_localizationManager = localizationManager;
_fileSystem = fileSystem;
+ _serverConfigurationManager = serverConfigurationManager;
}
- public object Get(GetMetadataEditorInfo request)
+ /// <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 ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
{
- var item = _libraryManager.GetItemById(request.ItemId);
-
- var info = new MetadataEditorInfo
- {
- ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
- ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
- Countries = _localizationManager.GetCountries().ToArray(),
- Cultures = _localizationManager.GetCultures().ToArray()
- };
-
- if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName) &&
- item.SourceType == SourceType.Library)
- {
- var inheritedContentType = _libraryManager.GetInheritedContentType(item);
- var configuredContentType = _libraryManager.GetConfiguredContentType(item);
-
- if (string.IsNullOrWhiteSpace(inheritedContentType) || !string.IsNullOrWhiteSpace(configuredContentType))
- {
- info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
- info.ContentType = configuredContentType;
-
- if (string.IsNullOrWhiteSpace(inheritedContentType) || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
- {
- info.ContentTypeOptions = info.ContentTypeOptions
- .Where(i => string.IsNullOrWhiteSpace(i.Value) || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
- .ToArray();
- }
- }
- }
-
- return ToOptimizedResult(info);
- }
-
- public void Post(UpdateItemContentType request)
- {
- var item = _libraryManager.GetItemById(request.ItemId);
- var path = item.ContainingFolderPath;
-
- var types = ServerConfigurationManager.Configuration.ContentTypes
- .Where(i => !string.IsNullOrWhiteSpace(i.Name))
- .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
- .ToList();
-
- if (!string.IsNullOrWhiteSpace(request.ContentType))
- {
- types.Add(new NameValuePair
- {
- Name = path,
- Value = request.ContentType
- });
- }
-
- ServerConfigurationManager.Configuration.ContentTypes = types.ToArray();
- ServerConfigurationManager.SaveConfiguration();
- }
-
- private List<NameValuePair> GetContentTypeOptions(bool isForItem)
- {
- var list = new List<NameValuePair>();
-
- if (isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "Inherit",
- Value = ""
- });
- }
-
- list.Add(new NameValuePair
- {
- Name = "Movies",
- Value = "movies"
- });
- list.Add(new NameValuePair
- {
- Name = "Music",
- Value = "music"
- });
- list.Add(new NameValuePair
- {
- Name = "Shows",
- Value = "tvshows"
- });
-
- if (!isForItem)
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
{
- list.Add(new NameValuePair
- {
- Name = "Books",
- Value = "books"
- });
+ return NotFound();
}
- list.Add(new NameValuePair
- {
- Name = "HomeVideos",
- Value = "homevideos"
- });
- list.Add(new NameValuePair
- {
- Name = "MusicVideos",
- Value = "musicvideos"
- });
- list.Add(new NameValuePair
- {
- Name = "Photos",
- Value = "photos"
- });
-
- if (!isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "MixedContent",
- Value = ""
- });
- }
-
- foreach (var val in list)
- {
- val.Name = _localizationManager.GetLocalizedString(val.Name);
- }
-
- return list;
- }
-
- public void Post(UpdateItem request)
- {
- var item = _libraryManager.GetItemById(request.ItemId);
-
var newLockData = request.LockData ?? false;
var isLockedChanged = item.IsLocked != newLockData;
var series = item as Series;
- var displayOrderChanged = series != null && !string.Equals(series.DisplayOrder ?? string.Empty, request.DisplayOrder ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ var displayOrderChanged = series != null && !string.Equals(
+ series.DisplayOrder ?? string.Empty,
+ request.DisplayOrder ?? string.Empty,
+ StringComparison.OrdinalIgnoreCase);
// Do this first so that metadata savers can pull the updates from the database.
if (request.People != null)
{
- _libraryManager.UpdatePeople(item, request.People.Select(x => new PersonInfo { Name = x.Name, Role = x.Role, Type = x.Type }).ToList());
+ _libraryManager.UpdatePeople(
+ item,
+ request.People.Select(x => new PersonInfo
+ {
+ Name = x.Name,
+ Role = x.Role,
+ Type = x.Type
+ }).ToList());
}
UpdateItem(request, item);
@@ -232,7 +117,7 @@ namespace MediaBrowser.Api
if (displayOrderChanged)
{
_providerManager.QueueRefresh(
- series.Id,
+ series!.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
@@ -241,11 +126,101 @@ namespace MediaBrowser.Api
},
RefreshPriority.High);
}
+
+ return NoContent();
}
- private DateTime NormalizeDateTime(DateTime val)
+ /// <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] Guid itemId)
{
- return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+ var item = _libraryManager.GetItemById(itemId);
+
+ var info = new MetadataEditorInfo
+ {
+ ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
+ ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
+ Countries = _localizationManager.GetCountries().ToArray(),
+ Cultures = _localizationManager.GetCultures().ToArray()
+ };
+
+ if (!item.IsVirtualItem
+ && !(item is ICollectionFolder)
+ && !(item is UserView)
+ && !(item is AggregateFolder)
+ && !(item is LiveTvChannel)
+ && !(item is IItemByName)
+ && item.SourceType == SourceType.Library)
+ {
+ var inheritedContentType = _libraryManager.GetInheritedContentType(item);
+ var configuredContentType = _libraryManager.GetConfiguredContentType(item);
+
+ if (string.IsNullOrWhiteSpace(inheritedContentType) ||
+ !string.IsNullOrWhiteSpace(configuredContentType))
+ {
+ info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
+ info.ContentType = configuredContentType;
+
+ if (string.IsNullOrWhiteSpace(inheritedContentType)
+ || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ {
+ info.ContentTypeOptions = info.ContentTypeOptions
+ .Where(i => string.IsNullOrWhiteSpace(i.Value)
+ || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+ }
+ }
+ }
+
+ return info;
+ }
+
+ /// <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] Guid itemId, [FromQuery, Required] string? contentType)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ var path = item.ContainingFolderPath;
+
+ var types = _serverConfigurationManager.Configuration.ContentTypes
+ .Where(i => !string.IsNullOrWhiteSpace(i.Name))
+ .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (!string.IsNullOrWhiteSpace(contentType))
+ {
+ types.Add(new NameValuePair
+ {
+ Name = path,
+ Value = contentType
+ });
+ }
+
+ _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
+ _serverConfigurationManager.SaveConfiguration();
+ return NoContent();
}
private void UpdateItem(BaseItemDto request, BaseItem item)
@@ -361,24 +336,25 @@ namespace MediaBrowser.Api
}
}
- if (item is Audio song)
- {
- song.Album = request.Album;
- }
-
- if (item is MusicVideo musicVideo)
+ switch (item)
{
- musicVideo.Album = request.Album;
- }
+ case Audio song:
+ song.Album = request.Album;
+ break;
+ case MusicVideo musicVideo:
+ musicVideo.Album = request.Album;
+ break;
+ case Series series:
+ {
+ series.Status = GetSeriesStatus(request);
- if (item is Series series)
- {
- series.Status = GetSeriesStatus(request);
+ if (request.AirDays != null)
+ {
+ series.AirDays = request.AirDays;
+ series.AirTime = request.AirTime;
+ }
- if (request.AirDays != null)
- {
- series.AirDays = request.AirDays;
- series.AirTime = request.AirTime;
+ break;
}
}
}
@@ -392,5 +368,81 @@ namespace MediaBrowser.Api
return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
}
+
+ private DateTime NormalizeDateTime(DateTime val)
+ {
+ return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+ }
+
+ private List<NameValuePair> GetContentTypeOptions(bool isForItem)
+ {
+ var list = new List<NameValuePair>();
+
+ if (isForItem)
+ {
+ list.Add(new NameValuePair
+ {
+ Name = "Inherit",
+ Value = string.Empty
+ });
+ }
+
+ list.Add(new NameValuePair
+ {
+ Name = "Movies",
+ Value = "movies"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "Music",
+ Value = "music"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "Shows",
+ Value = "tvshows"
+ });
+
+ if (!isForItem)
+ {
+ list.Add(new NameValuePair
+ {
+ Name = "Books",
+ Value = "books"
+ });
+ }
+
+ list.Add(new NameValuePair
+ {
+ Name = "HomeVideos",
+ Value = "homevideos"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "MusicVideos",
+ Value = "musicvideos"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "Photos",
+ Value = "photos"
+ });
+
+ if (!isForItem)
+ {
+ list.Add(new NameValuePair
+ {
+ Name = "MixedContent",
+ Value = string.Empty
+ });
+ }
+
+ foreach (var val in list)
+ {
+ val.Name = _localizationManager.GetLocalizedString(val.Name);
+ }
+
+ return list;
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
new file mode 100644
index 000000000..1b8b68313
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -0,0 +1,593 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The items controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ 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;
+
+ /// <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>
+ public ItemsController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ IDtoService dtoService,
+ ILogger<ItemsController> logger)
+ {
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _localization = localization;
+ _dtoService = dtoService;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Gets items based on a query.
+ /// </summary>
+ /// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
+ /// <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 delimeted.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</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="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</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 delimeted. 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 delimeted.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. 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 delimeted. 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 delimeted.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</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 delimeted.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</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 delimeted.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</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 delimeted.</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 delimeted.</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 delimeted.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</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")]
+ [HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetItems(
+ [FromRoute] Guid? uId,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] string? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery] string? locationTypes,
+ [FromQuery] string? 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] string? excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? sortOrder,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? imageTypes,
+ [FromQuery] string? sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery] string? genres,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? artists,
+ [FromQuery] string? excludeArtistIds,
+ [FromQuery] string? artistIds,
+ [FromQuery] string? albumArtistIds,
+ [FromQuery] string? contributingArtistIds,
+ [FromQuery] string? albums,
+ [FromQuery] string? albumIds,
+ [FromQuery] string? ids,
+ [FromQuery] string? 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] string? seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] string? studioIds,
+ [FromQuery] string? genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ // use user id route parameter over query parameter
+ userId = uId ?? userId;
+
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
+ {
+ parentId = null;
+ }
+
+ BaseItem? item = null;
+ QueryResult<BaseItem> result;
+ if (!string.IsNullOrEmpty(parentId))
+ {
+ item = _libraryManager.GetItemById(parentId);
+ }
+
+ item ??= _libraryManager.GetUserRootFolder();
+
+ if (!(item is Folder folder))
+ {
+ folder = _libraryManager.GetUserRootFolder();
+ }
+
+ if (folder is IHasCollectionType hasCollectionType
+ && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ {
+ recursive = true;
+ includeItemTypes = "Playlist";
+ }
+
+ bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
+ // Assume all folders inside an EnabledChannel are enabled
+ || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
+
+ var collectionFolders = _libraryManager.GetCollectionFolders(item);
+ foreach (var collectionFolder in collectionFolders)
+ {
+ if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
+ collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
+ StringComparer.OrdinalIgnoreCase))
+ {
+ isInEnabledFolder = true;
+ }
+ }
+
+ if (!(item is UserRootFolder)
+ && !isInEnabledFolder
+ && !user.HasPermission(PermissionKind.EnableAllFolders)
+ && !user.HasPermission(PermissionKind.EnableAllChannels))
+ {
+ _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) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder))
+ {
+ var query = new InternalItemsQuery(user!)
+ {
+ IsPlayed = isPlayed,
+ MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ 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,
+ HasOverview = hasOverview,
+ HasOfficialRating = hasOfficialRating,
+ HasParentalRating = hasParentalRating,
+ HasSpecialFeature = hasSpecialFeature,
+ HasSubtitles = hasSubtitles,
+ HasThemeSong = hasThemeSong,
+ HasThemeVideo = hasThemeVideo,
+ HasTrailer = hasTrailer,
+ IsHD = isHd,
+ Is4K = is4K,
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
+ ArtistIds = RequestHelpers.GetGuids(artistIds),
+ AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds),
+ ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds),
+ GenreIds = RequestHelpers.GetGuids(genreIds),
+ StudioIds = RequestHelpers.GetGuids(studioIds),
+ Person = person,
+ PersonIds = RequestHelpers.GetGuids(personIds),
+ PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+ Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(),
+ VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
+ AdjacentTo = adjacentTo,
+ ItemIds = RequestHelpers.GetGuids(ids),
+ MinCommunityRating = minCommunityRating,
+ MinCriticRating = minCriticRating,
+ ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
+ ParentIndexNumber = parentIndexNumber,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
+ MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
+ MinPremiereDate = minPremiereDate?.ToUniversalTime(),
+ MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
+ };
+
+ if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))
+ {
+ query.CollapseBoxSetItems = false;
+ }
+
+ foreach (var filter in RequestHelpers.GetFilters(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;
+ }
+ }
+
+ // Filter by Series Status
+ if (!string.IsNullOrEmpty(seriesStatus))
+ {
+ query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
+ }
+
+ // ExcludeLocationTypes
+ if (!string.IsNullOrEmpty(excludeLocationTypes))
+ {
+ if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual))
+ {
+ query.IsVirtualItem = false;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(locationTypes))
+ {
+ var requestedLocationTypes = locationTypes.Split(',');
+ if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
+ {
+ query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
+ }
+ }
+
+ // 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 (!string.IsNullOrEmpty(artists))
+ {
+ query.ArtistIds = artists.Split('|').Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetArtist(i, new DtoOptions(false));
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != null).Select(i => i!.Id).ToArray();
+ }
+
+ // ExcludeArtistIds
+ if (!string.IsNullOrWhiteSpace(excludeArtistIds))
+ {
+ query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+ }
+
+ if (!string.IsNullOrWhiteSpace(albumIds))
+ {
+ query.AlbumIds = RequestHelpers.GetGuids(albumIds);
+ }
+
+ // Albums
+ if (!string.IsNullOrEmpty(albums))
+ {
+ query.AlbumIds = albums.Split('|').SelectMany(i =>
+ {
+ return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
+ }).ToArray();
+ }
+
+ // Studios
+ if (!string.IsNullOrEmpty(studios))
+ {
+ query.StudioIds = studios.Split('|').Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(i);
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != 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 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
+ {
+ query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) };
+ }
+ }
+
+ result = folder.GetItems(query);
+ }
+ else
+ {
+ var itemsArray = folder.GetChildren(user, true);
+ result = new QueryResult<BaseItem> { Items = itemsArray, TotalRecordCount = itemsArray.Count, StartIndex = 0 };
+ }
+
+ return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
+ }
+
+ /// <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 delimeted. 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 delimeted.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</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] Guid userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ var user = _userManager.GetUserById(userId);
+ var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ var ancestorIds = Array.Empty<Guid>();
+
+ var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
+ if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
+ {
+ ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ .Where(i => i is Folder)
+ .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Select(i => i.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 = RequestHelpers.Split(mediaTypes, ',', true),
+ IsVirtualItem = false,
+ CollapseBoxSetItems = false,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ AncestorIds = ancestorIds,
+ IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ SearchTerm = searchTerm
+ });
+
+ var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
+
+ return new QueryResult<BaseItemDto>
+ {
+ StartIndex = startIndex.GetValueOrDefault(),
+ TotalRecordCount = itemsResult.TotalRecordCount,
+ Items = returnItems
+ };
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
new file mode 100644
index 000000000..4548e202a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -0,0 +1,1036 @@
+using System;
+using System.Collections.Generic;
+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.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.LibraryDtos;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Movie = Jellyfin.Data.Entities.Movie;
+using MusicAlbum = Jellyfin.Data.Entities.MusicAlbum;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <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 IAuthorizationContext _authContext;
+ 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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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,
+ IAuthorizationContext authContext,
+ IActivityManager activityManager,
+ ILocalizationManager localization,
+ ILibraryMonitor libraryMonitor,
+ ILogger<LibraryController> logger,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _providerManager = providerManager;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _authContext = authContext;
+ _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)]
+ public ActionResult GetFile([FromRoute] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ using var fileStream = new FileStream(item.Path, FileMode.Open, FileAccess.Read);
+ return File(fileStream, MimeTypes.GetMimeType(item.Path));
+ }
+
+ /// <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 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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<ThemeMediaResult> GetThemeSongs(
+ [FromRoute] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var item = itemId.Equals(Guid.Empty)
+ ? (!userId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.RootFolder)
+ : _libraryManager.GetItemById(itemId);
+
+ if (item == null)
+ {
+ return NotFound("Item not found.");
+ }
+
+ IEnumerable<BaseItem> themeItems;
+
+ while (true)
+ {
+ themeItems = item.GetThemeSongs();
+
+ if (themeItems.Any() || !inheritFromParent)
+ {
+ break;
+ }
+
+ var parent = item.GetParent();
+ if (parent == null)
+ {
+ break;
+ }
+
+ item = parent;
+ }
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var items = themeItems
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+ .ToArray();
+
+ return new ThemeMediaResult
+ {
+ Items = items,
+ TotalRecordCount = items.Length,
+ OwnerId = item.Id
+ };
+ }
+
+ /// <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] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var item = itemId.Equals(Guid.Empty)
+ ? (!userId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.RootFolder)
+ : _libraryManager.GetItemById(itemId);
+
+ if (item == null)
+ {
+ return NotFound("Item not found.");
+ }
+
+ IEnumerable<BaseItem> themeItems;
+
+ while (true)
+ {
+ themeItems = item.GetThemeVideos();
+
+ if (themeItems.Any() || !inheritFromParent)
+ {
+ break;
+ }
+
+ var parent = item.GetParent();
+ if (parent == null)
+ {
+ break;
+ }
+
+ item = parent;
+ }
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var items = themeItems
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+ .ToArray();
+
+ return new ThemeMediaResult
+ {
+ Items = items,
+ TotalRecordCount = items.Length,
+ OwnerId = item.Id
+ };
+ }
+
+ /// <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] 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>
+ [HttpGet("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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public ActionResult DeleteItem(Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ var auth = _authContext.GetAuthorizationInfo(Request);
+ var user = auth.User;
+
+ if (!item.CanDelete(user))
+ {
+ return Unauthorized("Unauthorized access");
+ }
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions { DeleteFileLocation = true },
+ true);
+
+ return NoContent();
+ }
+
+ /// <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] string? ids)
+ {
+ if (string.IsNullOrEmpty(ids))
+ {
+ return NoContent();
+ }
+
+ var itemIds = RequestHelpers.Split(ids, ',', true);
+ foreach (var i in itemIds)
+ {
+ var item = _libraryManager.GetItemById(i);
+ var auth = _authContext.GetAuthorizationInfo(Request);
+ var user = auth.User;
+
+ if (!item.CanDelete(user))
+ {
+ if (ids.Length > 1)
+ {
+ return Unauthorized("Unauthorized access");
+ }
+
+ continue;
+ }
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions { DeleteFileLocation = true },
+ true);
+ }
+
+ return NoContent();
+ }
+
+ /// <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.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var counts = new ItemCounts
+ {
+ AlbumCount = GetCount(typeof(MusicAlbum), user, isFavorite),
+ EpisodeCount = GetCount(typeof(Episode), user, isFavorite),
+ MovieCount = GetCount(typeof(Movie), user, isFavorite),
+ SeriesCount = GetCount(typeof(Series), user, isFavorite),
+ SongCount = GetCount(typeof(Audio), user, isFavorite),
+ MusicVideoCount = GetCount(typeof(MusicVideo), user, isFavorite),
+ BoxSetCount = GetCount(typeof(BoxSet), user, isFavorite),
+ BookCount = GetCount(typeof(Book), user, isFavorite)
+ };
+
+ return counts;
+ }
+
+ /// <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] Guid itemId, [FromQuery] Guid? userId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item == null)
+ {
+ return NotFound("Item not found");
+ }
+
+ var baseItemDtos = new List<BaseItemDto>();
+
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ BaseItem parent = item.GetParent();
+
+ while (parent != null)
+ {
+ if (user != null)
+ {
+ parent = TranslateParentItem(parent, user);
+ }
+
+ baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
+
+ parent = parent.GetParent();
+ }
+
+ return baseItemDtos;
+ }
+
+ /// <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));
+ }
+
+ /// <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.DefaultAuthorization)]
+ [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;
+
+ items = items.Where(i => i.IsHidden == val).ToList();
+ }
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var result = new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = items.Count,
+ Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray()
+ };
+
+ return result;
+ }
+
+ /// <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[] { nameof(Series) },
+ DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ }
+ }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
+
+ foreach (var item in series)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path);
+ }
+
+ return NoContent();
+ }
+
+ /// <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([FromRoute] string? tmdbId, [FromRoute] string? imdbId)
+ {
+ var movies = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { nameof(Movie) },
+ DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ }
+ });
+
+ if (!string.IsNullOrWhiteSpace(imdbId))
+ {
+ movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ else if (!string.IsNullOrWhiteSpace(tmdbId))
+ {
+ movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ else
+ {
+ movies = new List<BaseItem>();
+ }
+
+ foreach (var item in movies)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path);
+ }
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Reports that new movies have been added by an external source.
+ /// </summary>
+ /// <param name="updates">A list of updated media paths.</param>
+ /// <response code="204">Report success.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Media/Updated")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
+ {
+ foreach (var item in updates)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path);
+ }
+
+ return NoContent();
+ }
+
+ /// <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)]
+ public ActionResult GetDownload([FromRoute] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ var auth = _authContext.GetAuthorizationInfo(Request);
+
+ var user = auth.User;
+
+ if (user != null)
+ {
+ if (!item.CanDownload(user))
+ {
+ throw new ArgumentException("Item does not support downloading");
+ }
+ }
+ else
+ {
+ if (!item.CanDownload())
+ {
+ throw new ArgumentException("Item does not support downloading");
+ }
+ }
+
+ if (user != null)
+ {
+ LogDownload(item, user, auth);
+ }
+
+ var path = item.Path;
+
+ // Quotes are valid in linux. They'll possibly cause issues here
+ var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty, StringComparison.Ordinal);
+ if (!string.IsNullOrWhiteSpace(filename))
+ {
+ // Kestrel doesn't support non-ASCII characters in headers
+ if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]"))
+ {
+ // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
+ filename = WebUtility.UrlEncode(filename);
+ }
+ }
+
+ // TODO determine non-ASCII validity.
+ return PhysicalFile(path, MimeTypes.GetMimeType(path));
+ }
+
+ /// <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 = "GetSimilarArtists2")]
+ [HttpGet("Items/{itemId}/Similar")]
+ [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
+ [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
+ [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
+ [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
+ [FromRoute] Guid itemId,
+ [FromQuery] string? excludeArtistIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields)
+ {
+ var item = itemId.Equals(Guid.Empty)
+ ? (!userId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.RootFolder)
+ : _libraryManager.GetItemById(itemId);
+
+ var program = item as IHasProgramAttributes;
+ var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
+ if (program != null && program.IsSeries)
+ {
+ return GetSimilarItemsResult(
+ item,
+ excludeArtistIds,
+ userId,
+ limit,
+ fields,
+ new[] { nameof(Series) },
+ false);
+ }
+
+ if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
+ {
+ return new QueryResult<BaseItemDto>();
+ }
+
+ return GetSimilarItemsResult(
+ item,
+ excludeArtistIds,
+ userId,
+ limit,
+ fields,
+ new[] { item.GetType().Name },
+ isMovie);
+ }
+
+ /// <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)
+ {
+ var result = new LibraryOptionsResultDto();
+
+ var types = GetRepresentativeItemTypes(libraryContentType);
+ var typesList = types.ToList();
+
+ var plugins = _providerManager.GetAllMetadataPlugins()
+ .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase))
+ .OrderBy(i => typesList.IndexOf(i.ItemType))
+ .ToList();
+
+ result.MetadataSavers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
+ })
+ .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .Select(x => x.First())
+ .ToArray();
+
+ result.MetadataReaders = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .Select(x => x.First())
+ .ToArray();
+
+ result.SubtitleFetchers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .Select(x => x.First())
+ .ToArray();
+
+ var typeOptions = new List<LibraryTypeOptionsDto>();
+
+ foreach (var type in types)
+ {
+ TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
+
+ typeOptions.Add(new LibraryTypeOptionsDto
+ {
+ Type = type,
+
+ MetadataFetchers = plugins
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
+ })
+ .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .Select(x => x.First())
+ .ToArray(),
+
+ ImageFetchers = plugins
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
+ })
+ .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .Select(x => x.First())
+ .ToArray(),
+
+ SupportedImageTypes = plugins
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
+ .Distinct()
+ .ToArray(),
+
+ DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
+ });
+ }
+
+ result.TypeOptions = typeOptions.ToArray();
+
+ return result;
+ }
+
+ private int GetCount(Type type, User? user, bool? isFavorite)
+ {
+ var query = new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { type.Name },
+ Limit = 0,
+ Recursive = true,
+ IsVirtualItem = false,
+ IsFavorite = isFavorite,
+ DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ }
+ };
+
+ return _libraryManager.GetItemsResult(query).TotalRecordCount;
+ }
+
+ private BaseItem TranslateParentItem(BaseItem item, User user)
+ {
+ return item.GetParent() is AggregateFolder
+ ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
+ : item;
+ }
+
+ private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
+ {
+ try
+ {
+ _activityManager.Create(new ActivityLog(
+ string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
+ "UserDownloadingContent",
+ auth.UserId)
+ {
+ ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
+ });
+ }
+ catch
+ {
+ // Logged at lower levels
+ }
+ }
+
+ private QueryResult<BaseItemDto> GetSimilarItemsResult(
+ BaseItem item,
+ string? excludeArtistIds,
+ Guid? userId,
+ int? limit,
+ string? fields,
+ string[] includeItemTypes,
+ bool isMovie)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request);
+
+ var query = new InternalItemsQuery(user)
+ {
+ Limit = limit,
+ IncludeItemTypes = includeItemTypes,
+ IsMovie = isMovie,
+ SimilarTo = item,
+ DtoOptions = dtoOptions,
+ EnableTotalRecordCount = !isMovie,
+ EnableGroupByMetadataKey = isMovie
+ };
+
+ // ExcludeArtistIds
+ if (!string.IsNullOrEmpty(excludeArtistIds))
+ {
+ query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+ }
+
+ List<BaseItem> itemsResult;
+
+ if (isMovie)
+ {
+ var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(nameof(Trailer));
+ itemTypes.Add(nameof(LiveTvProgram));
+ }
+
+ query.IncludeItemTypes = itemTypes.ToArray();
+ itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
+ }
+ else if (item is MusicArtist)
+ {
+ query.IncludeItemTypes = Array.Empty<string>();
+
+ itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
+ }
+ else
+ {
+ itemsResult = _libraryManager.GetItemList(query);
+ }
+
+ var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
+
+ var result = new QueryResult<BaseItemDto>
+ {
+ Items = returnList,
+ TotalRecordCount = itemsResult.Count
+ };
+
+ return result;
+ }
+
+ private static string[] GetRepresentativeItemTypes(string? contentType)
+ {
+ return contentType switch
+ {
+ CollectionType.BoxSets => new[] { "BoxSet" },
+ CollectionType.Playlists => new[] { "Playlist" },
+ CollectionType.Movies => new[] { "Movie" },
+ CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
+ CollectionType.Books => new[] { "Book" },
+ CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
+ CollectionType.HomeVideos => new[] { "Video", "Photo" },
+ CollectionType.Photos => new[] { "Video", "Photo" },
+ CollectionType.MusicVideos => new[] { "MusicVideo" },
+ _ => new[] { "Series", "Season", "Episode", "Movie" }
+ };
+ }
+
+ private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
+ {
+ if (isNewLibrary)
+ {
+ return false;
+ }
+
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ .ToArray();
+
+ return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase));
+ }
+
+ private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ {
+ if (isNewLibrary)
+ {
+ if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
+ {
+ return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
+ }
+
+ return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
+ }
+
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ return metadataOptions.Length == 0
+ || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
+ }
+
+ private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ {
+ if (isNewLibrary)
+ {
+ if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
+ {
+ return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
+ }
+
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ if (metadataOptions.Length == 0)
+ {
+ return true;
+ }
+
+ return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
new file mode 100644
index 000000000..cdab4f356
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -0,0 +1,331 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.LibraryStructureDto;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <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>
+ /// 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;
+ }
+
+ /// <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] string? collectionType,
+ [FromQuery] string[] paths,
+ [FromBody] LibraryOptionsDto? libraryOptionsDto,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
+
+ if (paths != null && paths.Length > 0)
+ {
+ libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
+ }
+
+ await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <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();
+ }
+
+ /// <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));
+ }
+
+ if (string.IsNullOrWhiteSpace(newName))
+ {
+ throw new ArgumentNullException(nameof(newName));
+ }
+
+ var rootFolderPath = _appPaths.DefaultUserViewsPath;
+
+ var currentPath = Path.Combine(rootFolderPath, name);
+ var newPath = Path.Combine(rootFolderPath, newName);
+
+ if (!Directory.Exists(currentPath))
+ {
+ return NotFound("The media collection does not exist.");
+ }
+
+ if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
+ {
+ return Conflict($"The media library already exists at {newPath}.");
+ }
+
+ _libraryMonitor.Stop();
+
+ try
+ {
+ // Changing capitalization. Handle windows case insensitivity
+ if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
+ {
+ var tempPath = Path.Combine(
+ rootFolderPath,
+ Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
+ Directory.Move(currentPath, tempPath);
+ currentPath = tempPath;
+ }
+
+ Directory.Move(currentPath, newPath);
+ }
+ finally
+ {
+ CollectionFolder.OnCollectionFolderChange();
+
+ Task.Run(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();
+ }
+ });
+ }
+
+ return NoContent();
+ }
+
+ /// <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();
+
+ try
+ {
+ var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path };
+
+ _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();
+ }
+ });
+ }
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Updates a media path.
+ /// </summary>
+ /// <param name="name">The name of the library.</param>
+ /// <param name="pathInfo">The path info.</param>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ /// <response code="204">Media path updated.</response>
+ /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+ [HttpPost("Paths/Update")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateMediaPath(
+ [FromQuery] string? name,
+ [FromBody] MediaPathInfo? pathInfo)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ _libraryManager.UpdateMediaPath(name, 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))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ _libraryMonitor.Stop();
+
+ try
+ {
+ _libraryManager.RemoveMediaPath(name, path);
+ }
+ 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();
+ }
+ });
+ }
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Update library options.
+ /// </summary>
+ /// <param name="id">The library name.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <response code="204">Library updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("LibraryOptions")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateLibraryOptions(
+ [FromQuery] string? id,
+ [FromBody] LibraryOptions? libraryOptions)
+ {
+ var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
+
+ collectionFolder.UpdateLibraryOptions(libraryOptions);
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
new file mode 100644
index 000000000..3ccfc826d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -0,0 +1,1238 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Mime;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.LiveTvDtos;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
+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.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+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 ISessionContext _sessionContext;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IConfigurationManager _configurationManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+
+ /// <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="sessionContext">Instance of the <see cref="ISessionContext"/> 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>
+ public LiveTvController(
+ ILiveTvManager liveTvManager,
+ IUserManager userManager,
+ IHttpClientFactory httpClientFactory,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ ISessionContext sessionContext,
+ IMediaSourceManager mediaSourceManager,
+ IConfigurationManager configurationManager,
+ TranscodingJobHelper transcodingJobHelper)
+ {
+ _liveTvManager = liveTvManager;
+ _userManager = userManager;
+ _httpClientFactory = httpClientFactory;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _sessionContext = sessionContext;
+ _mediaSourceManager = mediaSourceManager;
+ _configurationManager = configurationManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ }
+
+ /// <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 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. 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="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] string? enableImageTypes,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] string? sortBy,
+ [FromQuery] SortOrder? sortOrder,
+ [FromQuery] bool enableFavoriteSorting = false,
+ [FromQuery] bool addCurrentProgram = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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 = RequestHelpers.Split(sortBy, ',', true),
+ SortOrder = sortOrder ?? SortOrder.Ascending,
+ AddCurrentProgram = addCurrentProgram
+ },
+ dtoOptions,
+ CancellationToken.None);
+
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ 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>
+ {
+ Items = returnArray,
+ TotalRecordCount = channelResult.TotalRecordCount
+ };
+ }
+
+ /// <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] Guid channelId, [FromQuery] Guid? userId)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var item = channelId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(channelId);
+
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(Request);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
+
+ /// <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. 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="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] string? enableImageTypes,
+ [FromQuery] string? 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()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .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 = RequestHelpers.GetItemFields(fields),
+ ImageTypeLimit = imageTypeLimit,
+ EnableImages = enableImages
+ }, dtoOptions);
+ }
+
+ /// <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. 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="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] string? enableImageTypes,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ return new QueryResult<BaseItemDto>();
+ }
+
+ /// <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>();
+ }
+
+ /// <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.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var folders = _liveTvManager.GetRecordingFolders(user);
+
+ var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = returnArray,
+ TotalRecordCount = returnArray.Count
+ };
+ }
+
+ /// <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] Guid recordingId, [FromQuery] Guid? userId)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var item = recordingId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(Request);
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
+
+ /// <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 ActionResult ResetTuner([FromRoute] string tunerId)
+ {
+ AssertUserCanManageLiveTv();
+ _liveTvManager.ResetTuner(tunerId, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <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(string timerId)
+ {
+ return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <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>
+ /// 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 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. 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="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] string? 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] string? sortBy,
+ [FromQuery] string? sortOrder,
+ [FromQuery] string? genres,
+ [FromQuery] string? genreIds,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] Guid? librarySeriesId,
+ [FromQuery] string? fields,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var query = new InternalItemsQuery(user)
+ {
+ ChannelIds = RequestHelpers.Split(channelIds, ',', true)
+ .Select(i => new Guid(i)).ToArray(),
+ 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 = RequestHelpers.Split(genres, ',', true),
+ GenreIds = RequestHelpers.GetGuids(genreIds)
+ };
+
+ if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
+ {
+ query.IsSeries = true;
+
+ if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
+ {
+ query.Name = series.Name;
+ }
+ }
+
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return await _liveTvManager.GetPrograms(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.DefaultAuthorization)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
+ {
+ var user = body.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(body.UserId);
+
+ var query = new InternalItemsQuery(user)
+ {
+ ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true)
+ .Select(i => new Guid(i)).ToArray(),
+ 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 = RequestHelpers.Split(body.Genres, ',', true),
+ GenreIds = RequestHelpers.GetGuids(body.GenreIds)
+ };
+
+ if (!body.LibrarySeriesId.Equals(Guid.Empty))
+ {
+ query.IsSeries = true;
+
+ if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
+ {
+ query.Name = series.Name;
+ }
+ }
+
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(body.Fields)
+ .AddClientFields(Request)
+ .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. 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="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 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] string? enableImageTypes,
+ [FromQuery] string? genreIds,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var query = new InternalItemsQuery(user)
+ {
+ IsAiring = isAiring,
+ Limit = limit,
+ HasAired = hasAired,
+ IsSeries = isSeries,
+ IsMovie = isMovie,
+ IsKids = isKids,
+ IsNews = isNews,
+ IsSports = isSports,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ GenreIds = RequestHelpers.GetGuids(genreIds)
+ };
+
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);
+ }
+
+ /// <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] string programId,
+ [FromQuery] Guid? userId)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ 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 ActionResult DeleteRecording([FromRoute] Guid recordingId)
+ {
+ AssertUserCanManageLiveTv();
+
+ var item = _libraryManager.GetItemById(recordingId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ _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] string timerId)
+ {
+ AssertUserCanManageLiveTv();
+ 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.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> UpdateTimer([FromRoute] string timerId, [FromBody] TimerInfoDto timerInfo)
+ {
+ AssertUserCanManageLiveTv();
+ 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.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
+ {
+ AssertUserCanManageLiveTv();
+ await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <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] string timerId)
+ {
+ var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+ if (timer == null)
+ {
+ return NotFound();
+ }
+
+ return timer;
+ }
+
+ /// <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)
+ {
+ return await _liveTvManager.GetSeriesTimers(
+ new SeriesTimerQuery
+ {
+ SortOrder = sortOrder ?? SortOrder.Ascending,
+ SortBy = sortBy
+ }, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <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] string timerId)
+ {
+ AssertUserCanManageLiveTv();
+ await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <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] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
+ {
+ AssertUserCanManageLiveTv();
+ await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
+ 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)
+ {
+ AssertUserCanManageLiveTv();
+ await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, 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] Guid? groupId)
+ {
+ 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();
+ }
+
+ /// <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>
+ /// 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>
+ /// 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>
+ /// 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)
+ {
+ using var sha = SHA1.Create();
+ if (!string.IsNullOrEmpty(pw))
+ {
+ listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
+ }
+
+ return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
+ }
+
+ /// <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>
+ /// 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)
+ {
+ return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
+ }
+
+ /// <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)]
+ public async Task<ActionResult> GetSchedulesDirectCountries()
+ {
+ var client = _httpClientFactory.CreateClient();
+ // 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("https://json.schedulesdirect.org/20141201/available/countries")
+ .ConfigureAwait(false);
+
+ return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
+ }
+
+ /// <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");
+
+ var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
+
+ var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
+
+ var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var mappings = listingsProviderInfo.ChannelMappings;
+
+ 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
+ };
+ }
+
+ /// <summary>
+ /// Set channel mappings.
+ /// </summary>
+ /// <param name="providerId">Provider id.</param>
+ /// <param name="tunerChannelId">Tuner channel id.</param>
+ /// <param name="providerChannelId">Provider channel id.</param>
+ /// <response code="200">Created channel mapping returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
+ [HttpPost("ChannelMappings")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping(
+ [FromQuery] string? providerId,
+ [FromQuery] string? tunerChannelId,
+ [FromQuery] string? providerChannelId)
+ {
+ return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).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();
+ }
+
+ /// <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")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [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 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)]
+ public async Task<ActionResult> GetLiveRecordingFile([FromRoute] string recordingId)
+ {
+ var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return NotFound();
+ }
+
+ await using var memoryStream = new MemoryStream();
+ await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None)
+ .WriteToAsync(memoryStream, CancellationToken.None)
+ .ConfigureAwait(false);
+ return File(memoryStream, MimeTypes.GetMimeType(path));
+ }
+
+ /// <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)]
+ public async Task<ActionResult> GetLiveStreamFile([FromRoute] string streamId, [FromRoute] string container)
+ {
+ var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
+ if (liveStreamInfo == null)
+ {
+ return NotFound();
+ }
+
+ await using var memoryStream = new MemoryStream();
+ await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
+ .WriteToAsync(memoryStream, CancellationToken.None)
+ .ConfigureAwait(false);
+ return File(memoryStream, MimeTypes.GetMimeType("file." + container));
+ }
+
+ private void AssertUserCanManageLiveTv()
+ {
+ var user = _sessionContext.GetUser(Request);
+
+ if (user == null)
+ {
+ throw new SecurityException("Anonymous live tv management is not allowed.");
+ }
+
+ if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
+ {
+ throw new SecurityException("The current user does not have permission to manage live tv.");
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
new file mode 100644
index 000000000..ef2e7e8b1
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Localization controller.
+ /// </summary>
+ [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
+ public class LocalizationController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <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 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());
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
new file mode 100644
index 000000000..7faf479a5
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -0,0 +1,315 @@
+using System;
+using System.Buffers;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.MediaInfoDtos;
+using Jellyfin.Api.Models.VideoDtos;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The media info controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class MediaInfoController : BaseJellyfinApiController
+ {
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IDeviceManager _deviceManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IAuthorizationContext _authContext;
+ private readonly ILogger<MediaInfoController> _logger;
+ private readonly 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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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,
+ IAuthorizationContext authContext,
+ ILogger<MediaInfoController> logger,
+ MediaInfoHelper mediaInfoHelper)
+ {
+ _mediaSourceManager = mediaSourceManager;
+ _deviceManager = deviceManager;
+ _libraryManager = libraryManager;
+ _authContext = authContext;
+ _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] 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>
+ /// <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="deviceProfile">The device profile.</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>
+ /// <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] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? liveStreamId,
+ [FromBody] DeviceProfileDto? deviceProfile,
+ [FromQuery] bool autoOpenLiveStream = false,
+ [FromQuery] bool enableDirectPlay = true,
+ [FromQuery] bool enableDirectStream = true,
+ [FromQuery] bool enableTranscoding = true,
+ [FromQuery] bool allowVideoStreamCopy = true,
+ [FromQuery] bool allowAudioStreamCopy = true)
+ {
+ var authInfo = _authContext.GetAuthorizationInfo(Request);
+
+ var profile = deviceProfile?.DeviceProfile;
+
+ _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
+
+ if (profile == null)
+ {
+ var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+ if (caps != null)
+ {
+ profile = caps.DeviceProfile;
+ }
+ }
+
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId,
+ liveStreamId)
+ .ConfigureAwait(false);
+
+ if (profile != null)
+ {
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
+
+ foreach (var mediaSource in info.MediaSources)
+ {
+ _mediaInfoHelper.SetDeviceSpecificData(
+ item,
+ mediaSource,
+ profile,
+ authInfo,
+ maxStreamingBitrate ?? profile.MaxStreamingBitrate,
+ startTimeTicks ?? 0,
+ mediaSourceId ?? string.Empty,
+ audioStreamIndex,
+ subtitleStreamIndex,
+ maxAudioChannels,
+ info!.PlaySessionId!,
+ userId ?? Guid.Empty,
+ enableDirectPlay,
+ enableDirectStream,
+ enableTranscoding,
+ allowVideoStreamCopy,
+ allowAudioStreamCopy,
+ Request.HttpContext.Connection.RemoteIpAddress.ToString());
+ }
+
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+ }
+
+ if (autoOpenLiveStream)
+ {
+ var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
+
+ if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
+ {
+ var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
+ Request,
+ new LiveStreamRequest
+ {
+ AudioStreamIndex = audioStreamIndex,
+ DeviceProfile = deviceProfile?.DeviceProfile,
+ EnableDirectPlay = enableDirectPlay,
+ EnableDirectStream = enableDirectStream,
+ 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 };
+ }
+ }
+
+ if (info.MediaSources != null)
+ {
+ foreach (var mediaSource in info.MediaSources)
+ {
+ _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
+ }
+ }
+
+ 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] long? maxStreamingBitrate,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] Guid? itemId,
+ [FromBody] OpenLiveStreamDto openLiveStreamDto,
+ [FromQuery] bool enableDirectPlay = true,
+ [FromQuery] bool enableDirectStream = true)
+ {
+ var request = new LiveStreamRequest
+ {
+ OpenToken = openToken,
+ UserId = userId ?? Guid.Empty,
+ PlaySessionId = playSessionId,
+ MaxStreamingBitrate = maxStreamingBitrate,
+ StartTimeTicks = startTimeTicks,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ MaxAudioChannels = maxAudioChannels,
+ ItemId = itemId ?? Guid.Empty,
+ DeviceProfile = openLiveStreamDto?.DeviceProfile,
+ EnableDirectPlay = enableDirectPlay,
+ EnableDirectStream = enableDirectStream,
+ DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
+ };
+ return await _mediaInfoHelper.OpenMediaSource(Request, 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 ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId)
+ {
+ _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
+ 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>
+ /// <response code="400">Size has to be a numer between 0 and 10,000,000.</response>
+ /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
+ [HttpGet("Playback/BitrateTest")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [Produces(MediaTypeNames.Application.Octet)]
+ public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
+ {
+ const int MaxSize = 10_000_000;
+
+ if (size <= 0)
+ {
+ return BadRequest($"The requested size ({size}) is equal to or smaller than 0.");
+ }
+
+ if (size > MaxSize)
+ {
+ return BadRequest($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).");
+ }
+
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
+ try
+ {
+ new Random().NextBytes(buffer);
+ return File(buffer, MediaTypeNames.Application.Octet);
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
new file mode 100644
index 000000000..148d8a18e
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Movies controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class MoviesController : BaseJellyfinApiController
+ {
+ 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] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] int categoryLimit = 5,
+ [FromQuery] int itemLimit = 8)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request);
+
+ var categories = new List<RecommendationDto>();
+
+ var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+
+ var query = new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[]
+ {
+ nameof(Movie),
+ // typeof(Trailer).Name,
+ // typeof(LiveTvProgram).Name
+ },
+ // IsMovie = true
+ OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+ Limit = 7,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ IsPlayed = true,
+ DtoOptions = dtoOptions
+ };
+
+ var recentlyPlayedMovies = _libraryManager.GetItemList(query);
+
+ var itemTypes = new List<string> { nameof(Movie) };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(nameof(Trailer));
+ itemTypes.Add(nameof(LiveTvProgram));
+ }
+
+ var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+ Limit = 10,
+ IsFavoriteOrLiked = true,
+ ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
+ EnableGroupByMetadataKey = true,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ DtoOptions = dtoOptions
+ });
+
+ var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
+ // Get recently played directors
+ var recentDirectors = GetDirectors(mostRecentMovies)
+ .ToList();
+
+ // Get recently played actors
+ var recentActors = GetActors(mostRecentMovies)
+ .ToList();
+
+ var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
+ var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
+
+ var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
+ var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
+
+ var categoryTypes = new List<IEnumerator<RecommendationDto>>
+ {
+ // Give this extra weight
+ similarToRecentlyPlayed,
+ similarToRecentlyPlayed,
+
+ // Give this extra weight
+ similarToLiked,
+ similarToLiked,
+ hasDirectorFromRecentlyPlayed,
+ hasActorFromRecentlyPlayed
+ };
+
+ while (categories.Count < categoryLimit)
+ {
+ var allEmpty = true;
+
+ foreach (var category in categoryTypes)
+ {
+ if (category.MoveNext())
+ {
+ categories.Add(category.Current);
+ allEmpty = false;
+
+ if (categories.Count >= categoryLimit)
+ {
+ break;
+ }
+ }
+ }
+
+ if (allEmpty)
+ {
+ break;
+ }
+ }
+
+ return Ok(categories.OrderBy(i => i.RecommendationType));
+ }
+
+ private IEnumerable<RecommendationDto> GetWithDirector(
+ User? user,
+ IEnumerable<string> names,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ RecommendationType type)
+ {
+ var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(nameof(Trailer));
+ itemTypes.Add(nameof(LiveTvProgram));
+ }
+
+ foreach (var name in names)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ Person = name,
+ // Account for duplicates by imdb id, since the database doesn't support this yet
+ Limit = itemLimit + 2,
+ PersonTypes = new[] { PersonType.Director },
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+ .Select(x => x.First())
+ .Take(itemLimit)
+ .ToList();
+
+ if (items.Count > 0)
+ {
+ var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
+
+ yield return new RecommendationDto
+ {
+ BaselineItemName = name,
+ CategoryId = name.GetMD5(),
+ RecommendationType = type,
+ Items = returnItems
+ };
+ }
+ }
+ }
+
+ private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ {
+ var itemTypes = new List<string> { nameof(Movie) };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(nameof(Trailer));
+ itemTypes.Add(nameof(LiveTvProgram));
+ }
+
+ foreach (var name in names)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ Person = name,
+ // Account for duplicates by imdb id, since the database doesn't support this yet
+ Limit = itemLimit + 2,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+ .Select(x => x.First())
+ .Take(itemLimit)
+ .ToList();
+
+ if (items.Count > 0)
+ {
+ var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
+
+ yield return new RecommendationDto
+ {
+ BaselineItemName = name,
+ CategoryId = name.GetMD5(),
+ RecommendationType = type,
+ Items = returnItems
+ };
+ }
+ }
+ }
+
+ private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ {
+ var itemTypes = new List<string> { nameof(Movie) };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(nameof(Trailer));
+ itemTypes.Add(nameof(LiveTvProgram));
+ }
+
+ foreach (var item in baselineItems)
+ {
+ var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ Limit = itemLimit,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ SimilarTo = item,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ });
+
+ if (similar.Count > 0)
+ {
+ var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
+
+ yield return new RecommendationDto
+ {
+ BaselineItemName = item.Name,
+ CategoryId = item.Id,
+ RecommendationType = type,
+ Items = returnItems
+ };
+ }
+ }
+ }
+
+ private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
+ {
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery
+ {
+ ExcludePersonTypes = new[] { PersonType.Director },
+ MaxListOrder = 3
+ });
+
+ var itemIds = items.Select(i => i.Id).ToList();
+
+ return people
+ .Where(i => itemIds.Contains(i.ItemId))
+ .Select(i => i.Name)
+ .DistinctNames();
+ }
+
+ private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
+ {
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery
+ {
+ PersonTypes = new[] { PersonType.Director }
+ });
+
+ var itemIds = items.Select(i => i.Id).ToList();
+
+ return people
+ .Where(i => itemIds.Contains(i.ItemId))
+ .Select(i => i.Name)
+ .DistinctNames();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
new file mode 100644
index 000000000..0d319137a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -0,0 +1,313 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The music genres controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class MusicGenresController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <summary>
+ /// Gets all music genres 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">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="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="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="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 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="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="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]
+ public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? genres,
+ [FromQuery] string? genreIds,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? studioIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem;
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ user = _userManager.GetUserById(userId.Value);
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+ }
+ else
+ {
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+ }
+
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
+ GenreIds = RequestHelpers.GetGuids(genreIds),
+ StudioIds = RequestHelpers.GetGuids(studioIds),
+ Person = person,
+ PersonIds = RequestHelpers.GetGuids(personIds),
+ PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+ Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ if (parentItem is Folder)
+ {
+ query.AncestorIds = new[] { new Guid(parentId) };
+ }
+ else
+ {
+ query.ItemIds = new[] { new Guid(parentId) };
+ }
+ }
+
+ // Studios
+ if (!string.IsNullOrEmpty(studios))
+ {
+ query.StudioIds = studios.Split('|')
+ .Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(i);
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != null)
+ .Select(i => i!.Id)
+ .ToArray();
+ }
+
+ foreach (var filter in RequestHelpers.GetFilters(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.GetMusicGenres(query);
+
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, counts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (!string.IsNullOrWhiteSpace(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>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
+
+ /// <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] string genreName, [FromQuery] Guid? userId)
+ {
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ MusicGenre item;
+
+ if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions);
+ }
+ else
+ {
+ item = _libraryManager.GetMusicGenre(genreName);
+ }
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
+ }
+
+ private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+ where T : BaseItem, new()
+ {
+ var result = libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '&'),
+ IncludeItemTypes = new[] { typeof(T).Name },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
+
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '/'),
+ IncludeItemTypes = new[] { typeof(T).Name },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
+
+ result ??= libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Name = name.Replace(BaseItem.SlugChar, '?'),
+ IncludeItemTypes = new[] { typeof(T).Name },
+ DtoOptions = dtoOptions
+ }).OfType<T>().FirstOrDefault();
+
+ return result;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
new file mode 100644
index 000000000..0ceda6815
--- /dev/null
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.NotificationDtos;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Library;
+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;
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NotificationsController" /> class.
+ /// </summary>
+ /// <param name="notificationManager">The notification manager.</param>
+ /// <param name="userManager">The user manager.</param>
+ public NotificationsController(INotificationManager notificationManager, IUserManager userManager)
+ {
+ _notificationManager = notificationManager;
+ _userManager = userManager;
+ }
+
+ /// <summary>
+ /// Gets a user's notifications.
+ /// </summary>
+ /// <response code="200">Notifications returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
+ [HttpGet("{userId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<NotificationResultDto> GetNotifications()
+ {
+ return new NotificationResultDto();
+ }
+
+ /// <summary>
+ /// Gets a user's notification summary.
+ /// </summary>
+ /// <response code="200">Summary of user's notifications returned.</response>
+ /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
+ [HttpGet("{userId}/Summary")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<NotificationsSummaryDto> GetNotificationsSummary()
+ {
+ return new NotificationsSummaryDto();
+ }
+
+ /// <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();
+ }
+
+ /// <summary>
+ /// Sends a notification to all admins.
+ /// </summary>
+ /// <param name="url">The URL of the notification.</param>
+ /// <param name="level">The level of the notification.</param>
+ /// <param name="name">The name of the notification.</param>
+ /// <param name="description">The description of the notification.</param>
+ /// <response code="204">Notification sent.</response>
+ /// <returns>A <cref see="NoContentResult"/>.</returns>
+ [HttpPost("Admin")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CreateAdminNotification(
+ [FromQuery] string? url,
+ [FromQuery] NotificationLevel? level,
+ [FromQuery] string name = "",
+ [FromQuery] string description = "")
+ {
+ var notification = new NotificationRequest
+ {
+ Name = name,
+ Description = description,
+ Url = url,
+ Level = level ?? NotificationLevel.Normal,
+ UserIds = _userManager.Users
+ .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
+ .Select(user => user.Id)
+ .ToArray(),
+ Date = DateTime.UtcNow,
+ };
+
+ _notificationManager.SendNotification(notification, CancellationToken.None);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Sets notifications as read.
+ /// </summary>
+ /// <response code="204">Notifications set as read.</response>
+ /// <returns>A <cref see="NoContentResult"/>.</returns>
+ [HttpPost("{userId}/Read")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SetRead()
+ {
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Sets notifications as unread.
+ /// </summary>
+ /// <response code="204">Notifications set as unread.</response>
+ /// <returns>A <cref see="NoContentResult"/>.</returns>
+ [HttpPost("{userId}/Unread")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SetUnread()
+ {
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
new file mode 100644
index 000000000..3d6a87909
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Updates;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Package Controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class PackageController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <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] string? assemblyGuid)
+ {
+ var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ var result = _installationManager.FilterPackages(
+ packages,
+ name,
+ string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
+
+ return result;
+ }
+
+ /// <summary>
+ /// Gets available packages.
+ /// </summary>
+ /// <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 packages;
+ }
+
+ /// <summary>
+ /// Installs a package.
+ /// </summary>
+ /// <param name="name">Package name.</param>
+ /// <param name="assemblyGuid">GUID of the associated assembly.</param>
+ /// <param name="version">Optional version. Defaults to latest version.</param>
+ /// <response code="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] string? assemblyGuid,
+ [FromQuery] string? version)
+ {
+ var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ var package = _installationManager.GetCompatibleVersions(
+ packages,
+ name,
+ string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid),
+ string.IsNullOrEmpty(version) ? null : Version.Parse(version)).FirstOrDefault();
+
+ if (package == null)
+ {
+ return NotFound();
+ }
+
+ await _installationManager.InstallPackage(package).ConfigureAwait(false);
+
+ return 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 _serverConfigurationManager.Configuration.PluginRepositories;
+ }
+
+ /// <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>
+ [HttpOptions("Repositories")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
+ {
+ _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
new file mode 100644
index 000000000..b6ccec666
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -0,0 +1,285 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Persons controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class PersonsController : BaseJellyfinApiController
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _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 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">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="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="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="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 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="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="enableImages">Optional, include image information in output.</param>
+ /// <param name="enableTotalRecordCount">Optional. Include total record count.</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] double? minCommunityRating,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? genres,
+ [FromQuery] string? genreIds,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? studioIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem;
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ user = _userManager.GetUserById(userId.Value);
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+ }
+ else
+ {
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+ }
+
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
+ GenreIds = RequestHelpers.GetGuids(genreIds),
+ StudioIds = RequestHelpers.GetGuids(studioIds),
+ Person = person,
+ PersonIds = RequestHelpers.GetGuids(personIds),
+ PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+ Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ if (parentItem is Folder)
+ {
+ query.AncestorIds = new[] { new Guid(parentId) };
+ }
+ else
+ {
+ query.ItemIds = new[] { new Guid(parentId) };
+ }
+ }
+
+ // Studios
+ if (!string.IsNullOrEmpty(studios))
+ {
+ query.StudioIds = studios.Split('|')
+ .Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(i);
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != null)
+ .Select(i => i!.Id)
+ .ToArray();
+ }
+
+ foreach (var filter in RequestHelpers.GetFilters(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 = new QueryResult<(BaseItem, ItemCounts)>();
+
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, counts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (!string.IsNullOrWhiteSpace(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>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
+
+ /// <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] string name, [FromQuery] Guid? userId)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(Request);
+
+ var item = _libraryManager.GetPerson(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ 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
new file mode 100644
index 000000000..12c87d7c3
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -0,0 +1,199 @@
+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.Models.PlaylistDtos;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Playlists;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Playlists controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class PlaylistsController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <summary>
+ /// Creates a new playlist.
+ /// </summary>
+ /// <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(
+ [FromBody, Required] CreatePlaylistDto createPlaylistRequest)
+ {
+ Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
+ var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
+ {
+ Name = createPlaylistRequest.Name,
+ ItemIdList = idGuidArray,
+ UserId = createPlaylistRequest.UserId,
+ MediaType = createPlaylistRequest.MediaType
+ }).ConfigureAwait(false);
+
+ return result;
+ }
+
+ /// <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 ActionResult AddToPlaylist(
+ [FromRoute] string? playlistId,
+ [FromQuery] string? ids,
+ [FromQuery] Guid? userId)
+ {
+ _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty);
+ return NoContent();
+ }
+
+ /// <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 ActionResult MoveItem(
+ [FromRoute] string? playlistId,
+ [FromRoute] string? itemId,
+ [FromRoute] int newIndex)
+ {
+ _playlistManager.MoveItem(playlistId, itemId, newIndex);
+ return NoContent();
+ }
+
+ /// <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 ActionResult RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds)
+ {
+ _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true));
+ 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. 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="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] Guid playlistId,
+ [FromRoute] Guid userId,
+ [FromRoute] int? startIndex,
+ [FromRoute] int? limit,
+ [FromRoute] string? fields,
+ [FromRoute] bool? enableImages,
+ [FromRoute] bool? enableUserData,
+ [FromRoute] int? imageTypeLimit,
+ [FromRoute] string? enableImageTypes)
+ {
+ var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
+ if (playlist == null)
+ {
+ return NotFound();
+ }
+
+ var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+
+ var items = playlist.GetManageableItems().ToArray();
+
+ var count = items.Length;
+
+ if (startIndex.HasValue)
+ {
+ items = items.Skip(startIndex.Value).ToArray();
+ }
+
+ if (limit.HasValue)
+ {
+ items = items.Take(limit.Value).ToArray();
+ }
+
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
+
+ for (int index = 0; index < dtos.Count; index++)
+ {
+ dtos[index].PlaylistItemId = items[index].Item1.Id;
+ }
+
+ var result = new QueryResult<BaseItemDto>
+ {
+ Items = dtos,
+ TotalRecordCount = count
+ };
+
+ return result;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
new file mode 100644
index 000000000..22f2ca5c3
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -0,0 +1,367 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Playstate controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class PlaystateController : BaseJellyfinApiController
+ {
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepository;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly IAuthorizationContext _authContext;
+ 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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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,
+ IAuthorizationContext authContext,
+ ILoggerFactory loggerFactory,
+ TranscodingJobHelper transcodingJobHelper)
+ {
+ _userManager = userManager;
+ _userDataRepository = userDataRepository;
+ _libraryManager = libraryManager;
+ _sessionManager = sessionManager;
+ _authContext = authContext;
+ _logger = loggerFactory.CreateLogger<PlaystateController>();
+
+ _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 ActionResult<UserItemDataDto> MarkPlayedItem(
+ [FromRoute] Guid userId,
+ [FromRoute] Guid itemId,
+ [FromQuery] DateTime? datePlayed)
+ {
+ var user = _userManager.GetUserById(userId);
+ var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ 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 dto;
+ }
+
+ /// <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 ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ var dto = UpdatePlayedStatus(user, itemId, false, null);
+ foreach (var additionalUserInfo in session.AdditionalUsers)
+ {
+ var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+ UpdatePlayedStatus(additionalUser, itemId, false, null);
+ }
+
+ 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 = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ 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 = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ 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] 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))
+ {
+ await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ }
+
+ playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ 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] Guid userId,
+ [FromRoute] 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
+ {
+ CanSeek = canSeek,
+ ItemId = itemId,
+ MediaSourceId = mediaSourceId,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ PlayMethod = playMethod,
+ PlaySessionId = playSessionId,
+ LiveStreamId = liveStreamId
+ };
+
+ playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
+ playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <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] Guid userId,
+ [FromRoute] 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
+ {
+ ItemId = itemId,
+ PositionTicks = positionTicks,
+ IsMuted = isMuted,
+ IsPaused = isPaused,
+ MediaSourceId = mediaSourceId,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ VolumeLevel = volumeLevel,
+ PlayMethod = playMethod,
+ PlaySessionId = playSessionId,
+ LiveStreamId = liveStreamId,
+ RepeatMode = repeatMode
+ };
+
+ playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
+ playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ 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] Guid userId,
+ [FromRoute] 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
+ };
+
+ _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
+ {
+ await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ }
+
+ playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <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)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (wasPlayed)
+ {
+ item.MarkPlayed(user, datePlayed, true);
+ }
+ else
+ {
+ item.MarkUnplayed(user);
+ }
+
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
+
+ private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
+ {
+ if (method == PlayMethod.Transcode)
+ {
+ var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
+ if (job == null)
+ {
+ return PlayMethod.DirectPlay;
+ }
+ }
+
+ return method;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
new file mode 100644
index 000000000..b2f34680b
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.PluginDtos;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Plugins;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Plugins controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class PluginsController : BaseJellyfinApiController
+ {
+ private readonly IApplicationHost _appHost;
+ private readonly IInstallationManager _installationManager;
+
+ private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginsController"/> class.
+ /// </summary>
+ /// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
+ /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+ public PluginsController(
+ IApplicationHost appHost,
+ IInstallationManager installationManager)
+ {
+ _appHost = appHost;
+ _installationManager = installationManager;
+ }
+
+ /// <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(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
+ }
+
+ /// <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 file could not be found.</returns>
+ [HttpDelete("{pluginId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult UninstallPlugin([FromRoute] Guid pluginId)
+ {
+ var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
+ if (plugin == null)
+ {
+ return NotFound();
+ }
+
+ _installationManager.UninstallPlugin(plugin);
+ return NoContent();
+ }
+
+ /// <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] Guid pluginId)
+ {
+ if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+ {
+ return NotFound();
+ }
+
+ return plugin.Configuration;
+ }
+
+ /// <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>
+ /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
+ /// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
+ /// when plugin not found or plugin doesn't have configuration.
+ /// </returns>
+ [HttpPost("{pluginId}/Configuration")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdatePluginConfiguration([FromRoute] Guid pluginId)
+ {
+ if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+ {
+ return NotFound();
+ }
+
+ var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
+ .ConfigureAwait(false);
+
+ plugin.UpdateConfiguration(configuration);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Get plugin security info.
+ /// </summary>
+ /// <response code="200">Plugin security info returned.</response>
+ /// <returns>Plugin security info.</returns>
+ [Obsolete("This endpoint should not be used.")]
+ [HttpGet("SecurityInfo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
+ {
+ return new PluginSecurityInfo
+ {
+ IsMbSupporter = true,
+ SupporterKey = "IAmTotallyLegit"
+ };
+ }
+
+ /// <summary>
+ /// Updates plugin security info.
+ /// </summary>
+ /// <param name="pluginSecurityInfo">Plugin security info.</param>
+ /// <response code="204">Plugin security info updated.</response>
+ /// <returns>An <see cref="NoContentResult"/>.</returns>
+ [Obsolete("This endpoint should not be used.")]
+ [HttpPost("SecurityInfo")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
+ {
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets registration status for a feature.
+ /// </summary>
+ /// <param name="name">Feature name.</param>
+ /// <response code="200">Registration status returned.</response>
+ /// <returns>Mb registration record.</returns>
+ [Obsolete("This endpoint should not be used.")]
+ [HttpPost("RegistrationRecords/{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string? name)
+ {
+ return new MBRegistrationRecord
+ {
+ IsRegistered = true,
+ RegChecked = true,
+ TrialVersion = false,
+ IsValid = true,
+ RegError = false
+ };
+ }
+
+ /// <summary>
+ /// Gets registration status for a feature.
+ /// </summary>
+ /// <param name="name">Feature name.</param>
+ /// <response code="501">Not implemented.</response>
+ /// <returns>Not Implemented.</returns>
+ /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
+ [Obsolete("Paid plugins are not supported")]
+ [HttpGet("Registrations/{name}")]
+ [ProducesResponseType(StatusCodes.Status501NotImplemented)]
+ public ActionResult GetRegistration([FromRoute] string? name)
+ {
+ // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
+ // delete all these registration endpoints. They are only kept for compatibility.
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
new file mode 100644
index 000000000..a203c50b9
--- /dev/null
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Remote Images Controller.
+ /// </summary>
+ [Route("")]
+ public class RemoteImageController : BaseJellyfinApiController
+ {
+ private readonly IProviderManager _providerManager;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IHttpClientFactory _httpClientFactory;
+ 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="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public RemoteImageController(
+ IProviderManager providerManager,
+ IServerApplicationPaths applicationPaths,
+ IHttpClientFactory httpClientFactory,
+ ILibraryManager libraryManager)
+ {
+ _providerManager = providerManager;
+ _applicationPaths = applicationPaths;
+ _httpClientFactory = httpClientFactory;
+ _libraryManager = libraryManager;
+ }
+
+ /// <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] Guid itemId,
+ [FromQuery] ImageType? type,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? providerName,
+ [FromQuery] bool includeAllLanguages = false)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == 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;
+ }
+
+ /// <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] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return Ok(_providerManager.GetRemoteImageProviderInfo(item));
+ }
+
+ /// <summary>
+ /// Gets a remote image.
+ /// </summary>
+ /// <param name="imageUrl">The image url.</param>
+ /// <response code="200">Remote image returned.</response>
+ /// <response code="404">Remote image not found.</response>
+ /// <returns>Image Stream.</returns>
+ [HttpGet("Images/Remote")]
+ [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
+ {
+ var urlHash = imageUrl.GetMD5();
+ var pointerCachePath = GetFullCachePath(urlHash.ToString());
+
+ string? contentPath = null;
+ var hasFile = false;
+
+ try
+ {
+ contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+ if (System.IO.File.Exists(contentPath))
+ {
+ hasFile = true;
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ // The file isn't cached yet
+ }
+ catch (IOException)
+ {
+ // The file isn't cached yet
+ }
+
+ if (!hasFile)
+ {
+ await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
+ contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+ }
+
+ if (string.IsNullOrEmpty(contentPath))
+ {
+ return NotFound();
+ }
+
+ var contentType = MimeTypes.GetMimeType(contentPath);
+ return File(System.IO.File.OpenRead(contentPath), contentType);
+ }
+
+ /// <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] Guid itemId,
+ [FromQuery, Required] ImageType type,
+ [FromQuery] string? imageUrl)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ 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);
+ }
+
+ /// <summary>
+ /// Downloads the image.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="urlHash">The URL hash.</param>
+ /// <param name="pointerCachePath">The pointer cache path.</param>
+ /// <returns>Task.</returns>
+ private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
+ {
+ var httpClient = _httpClientFactory.CreateClient();
+ using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
+ var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
+ var fullCachePath = GetFullCachePath(urlHash + "." + ext);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+ await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
+ Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+ await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
+ .ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
new file mode 100644
index 000000000..e672070c0
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Model.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Scheduled Tasks Controller.
+ /// </summary>
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public class ScheduledTasksController : BaseJellyfinApiController
+ {
+ private readonly ITaskManager _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<IScheduledTaskWorker> GetTasks(
+ [FromQuery] bool? isHidden,
+ [FromQuery] bool? isEnabled)
+ {
+ IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
+
+ foreach (var task in tasks)
+ {
+ if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
+ {
+ if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
+ {
+ continue;
+ }
+
+ if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
+ {
+ continue;
+ }
+ }
+
+ yield return task;
+ }
+ }
+
+ /// <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));
+
+ if (task == null)
+ {
+ return NotFound();
+ }
+
+ return ScheduledTaskHelpers.GetTaskInfo(task);
+ }
+
+ /// <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] string? taskId)
+ {
+ var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+ o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+
+ if (task == null)
+ {
+ return NotFound();
+ }
+
+ _taskManager.Execute(task, new TaskOptions());
+ return NoContent();
+ }
+
+ /// <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));
+
+ if (task == null)
+ {
+ return NotFound();
+ }
+
+ _taskManager.Cancel(task);
+ 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 == null)
+ {
+ return NotFound();
+ }
+
+ task.Triggers = triggerInfos;
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
new file mode 100644
index 000000000..e159a9666
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -0,0 +1,269 @@
+using System;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Search;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Search controller.
+ /// </summary>
+ [Route("Search/Hints")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class SearchController : BaseJellyfinApiController
+ {
+ private readonly ISearchEngine _searchEngine;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IImageProcessor _imageProcessor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SearchController"/> class.
+ /// </summary>
+ /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
+ public SearchController(
+ ISearchEngine searchEngine,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IImageProcessor imageProcessor)
+ {
+ _searchEngine = searchEngine;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _imageProcessor = imageProcessor;
+ }
+
+ /// <summary>
+ /// Gets the search hint result.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
+ /// <param name="searchTerm">The search term to filter on.</param>
+ /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param>
+ /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param>
+ /// <param name="parentId">If specified, only children of the parent are returned.</param>
+ /// <param name="isMovie">Optional filter for movies.</param>
+ /// <param name="isSeries">Optional filter for series.</param>
+ /// <param name="isNews">Optional filter for news.</param>
+ /// <param name="isKids">Optional filter for kids.</param>
+ /// <param name="isSports">Optional filter for sports.</param>
+ /// <param name="includePeople">Optional filter whether to include people.</param>
+ /// <param name="includeMedia">Optional filter whether to include media.</param>
+ /// <param name="includeGenres">Optional filter whether to include genres.</param>
+ /// <param name="includeStudios">Optional filter whether to include studios.</param>
+ /// <param name="includeArtists">Optional filter whether to include artists.</param>
+ /// <response code="200">Search hint returned.</response>
+ /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
+ [HttpGet]
+ [Description("Gets search hints based on a search term")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<SearchHintResult> Get(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] Guid? userId,
+ [FromQuery, Required] string? searchTerm,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? parentId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool includePeople = true,
+ [FromQuery] bool includeMedia = true,
+ [FromQuery] bool includeGenres = true,
+ [FromQuery] bool includeStudios = true,
+ [FromQuery] bool includeArtists = true)
+ {
+ var result = _searchEngine.GetSearchHints(new SearchQuery
+ {
+ Limit = limit,
+ SearchTerm = searchTerm,
+ IncludeArtists = includeArtists,
+ IncludeGenres = includeGenres,
+ IncludeMedia = includeMedia,
+ IncludePeople = includePeople,
+ IncludeStudios = includeStudios,
+ StartIndex = startIndex,
+ UserId = userId ?? Guid.Empty,
+ IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ ParentId = parentId,
+
+ IsKids = isKids,
+ IsMovie = isMovie,
+ IsNews = isNews,
+ IsSeries = isSeries,
+ IsSports = isSports
+ });
+
+ return new SearchHintResult
+ {
+ TotalRecordCount = result.TotalRecordCount,
+ SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
+ };
+ }
+
+ /// <summary>
+ /// Gets the search hint result.
+ /// </summary>
+ /// <param name="hintInfo">The hint info.</param>
+ /// <returns>SearchHintResult.</returns>
+ private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
+ {
+ var item = hintInfo.Item;
+
+ var result = new SearchHint
+ {
+ Name = item.Name,
+ IndexNumber = item.IndexNumber,
+ ParentIndexNumber = item.ParentIndexNumber,
+ Id = item.Id,
+ Type = item.GetClientTypeName(),
+ MediaType = item.MediaType,
+ MatchedTerm = hintInfo.MatchedTerm,
+ RunTimeTicks = item.RunTimeTicks,
+ ProductionYear = item.ProductionYear,
+ ChannelId = item.ChannelId,
+ EndDate = item.EndDate
+ };
+
+ // legacy
+ result.ItemId = result.Id;
+
+ if (item.IsFolder)
+ {
+ result.IsFolder = true;
+ }
+
+ var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
+
+ if (primaryImageTag != null)
+ {
+ result.PrimaryImageTag = primaryImageTag;
+ result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
+ }
+
+ SetThumbImageInfo(result, item);
+ SetBackdropImageInfo(result, item);
+
+ switch (item)
+ {
+ case IHasSeries hasSeries:
+ result.Series = hasSeries.SeriesName;
+ break;
+ case LiveTvProgram program:
+ result.StartDate = program.StartDate;
+ break;
+ case Series series:
+ if (series.Status.HasValue)
+ {
+ result.Status = series.Status.Value.ToString();
+ }
+
+ break;
+ case MusicAlbum album:
+ result.Artists = album.Artists;
+ result.AlbumArtist = album.AlbumArtist;
+ break;
+ case Audio song:
+ result.AlbumArtist = song.AlbumArtists?[0];
+ result.Artists = song.Artists;
+
+ MusicAlbum musicAlbum = song.AlbumEntity;
+
+ if (musicAlbum != null)
+ {
+ result.Album = musicAlbum.Name;
+ result.AlbumId = musicAlbum.Id;
+ }
+ else
+ {
+ result.Album = song.Album;
+ }
+
+ break;
+ }
+
+ if (!item.ChannelId.Equals(Guid.Empty))
+ {
+ var channel = _libraryManager.GetItemById(item.ChannelId);
+ result.ChannelName = channel?.Name;
+ }
+
+ return result;
+ }
+
+ private void SetThumbImageInfo(SearchHint hint, BaseItem item)
+ {
+ var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
+
+ if (itemWithImage == null && item is Episode)
+ {
+ itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
+ }
+
+ if (itemWithImage == null)
+ {
+ itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
+ }
+
+ if (itemWithImage != null)
+ {
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
+
+ if (tag != null)
+ {
+ hint.ThumbImageTag = tag;
+ hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+ }
+ }
+
+ private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
+ {
+ var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
+ ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
+
+ if (itemWithImage != null)
+ {
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
+
+ if (tag != null)
+ {
+ hint.BackdropImageTag = tag;
+ hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+ }
+ }
+
+ private T GetParentWithImage<T>(BaseItem item, ImageType type)
+ where T : BaseItem
+ {
+ return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
new file mode 100644
index 000000000..4795fb0cf
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -0,0 +1,491 @@
+#pragma warning disable CA1801
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The session controller.
+ /// </summary>
+ [Route("")]
+ public class SessionController : BaseJellyfinApiController
+ {
+ private readonly ISessionManager _sessionManager;
+ private readonly IUserManager _userManager;
+ private readonly IAuthorizationContext _authContext;
+ 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="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
+ /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+ public SessionController(
+ ISessionManager sessionManager,
+ IUserManager userManager,
+ IAuthorizationContext authContext,
+ IDeviceManager deviceManager)
+ {
+ _sessionManager = sessionManager;
+ _userManager = userManager;
+ _authContext = authContext;
+ _deviceManager = deviceManager;
+ }
+
+ /// <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)
+ {
+ var result = _sessionManager.Sessions;
+
+ if (!string.IsNullOrEmpty(deviceId))
+ {
+ result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ if (controllableByUserId.HasValue && !controllableByUserId.Equals(Guid.Empty))
+ {
+ result = result.Where(i => i.SupportsRemoteControl);
+
+ var user = _userManager.GetUserById(controllableByUserId.Value);
+
+ if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
+ {
+ result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId.Value));
+ }
+
+ if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
+ {
+ result = result.Where(i => !i.UserId.Equals(Guid.Empty));
+ }
+
+ if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
+ {
+ var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
+ result = result.Where(i => i.LastActivityDate >= minActiveDate);
+ }
+
+ result = result.Where(i =>
+ {
+ if (!string.IsNullOrWhiteSpace(i.DeviceId))
+ {
+ if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }
+
+ return Ok(result);
+ }
+
+ /// <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 ActionResult DisplayContent(
+ [FromRoute, Required] string? sessionId,
+ [FromQuery, Required] string? itemType,
+ [FromQuery, Required] string? itemId,
+ [FromQuery, Required] string? itemName)
+ {
+ var command = new BrowseRequest
+ {
+ ItemId = itemId,
+ ItemName = itemName,
+ ItemType = itemType
+ };
+
+ _sessionManager.SendBrowseCommand(
+ RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
+ sessionId,
+ command,
+ CancellationToken.None);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Instructs a session to play an item.
+ /// </summary>
+ /// <param name="sessionId">The session id.</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="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="playRequest">The <see cref="PlayRequest"/>.</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 ActionResult Play(
+ [FromRoute, Required] string? sessionId,
+ [FromQuery] Guid[] itemIds,
+ [FromQuery] long? startPositionTicks,
+ [FromQuery] PlayCommand playCommand,
+ [FromBody, Required] PlayRequest playRequest)
+ {
+ if (playRequest == null)
+ {
+ throw new ArgumentException("Request Body may not be null");
+ }
+
+ playRequest.ItemIds = itemIds;
+ playRequest.StartPositionTicks = startPositionTicks;
+ playRequest.PlayCommand = playCommand;
+
+ _sessionManager.SendPlayCommand(
+ RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
+ sessionId,
+ playRequest,
+ CancellationToken.None);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Issues a playstate command to a client.
+ /// </summary>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</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 ActionResult SendPlaystateCommand(
+ [FromRoute, Required] string? sessionId,
+ [FromBody] PlaystateRequest playstateRequest)
+ {
+ _sessionManager.SendPlaystateCommand(
+ RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
+ sessionId,
+ playstateRequest,
+ CancellationToken.None);
+
+ return NoContent();
+ }
+
+ /// <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 ActionResult SendSystemCommand(
+ [FromRoute, Required] string? sessionId,
+ [FromRoute, Required] string? command)
+ {
+ var name = command;
+ if (Enum.TryParse(name, true, out GeneralCommandType commandType))
+ {
+ name = commandType.ToString();
+ }
+
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ var generalCommand = new GeneralCommand
+ {
+ Name = name,
+ ControllingUserId = currentSession.UserId
+ };
+
+ _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
+
+ 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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendGeneralCommand(
+ [FromRoute, Required] string? sessionId,
+ [FromRoute, Required] string? command)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+
+ var generalCommand = new GeneralCommand
+ {
+ Name = command,
+ ControllingUserId = currentSession.UserId
+ };
+
+ _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
+
+ 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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendFullGeneralCommand(
+ [FromRoute, Required] string? sessionId,
+ [FromBody, Required] GeneralCommand command)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+
+ if (command == null)
+ {
+ throw new ArgumentException("Request body may not be null");
+ }
+
+ command.ControllingUserId = currentSession.UserId;
+
+ _sessionManager.SendGeneralCommand(
+ currentSession.Id,
+ sessionId,
+ command,
+ CancellationToken.None);
+
+ 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="text">The message test.</param>
+ /// <param name="header">The message header.</param>
+ /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
+ /// <response code="204">Message sent.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Sessions/{sessionId}/Message")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendMessageCommand(
+ [FromRoute, Required] string? sessionId,
+ [FromQuery, Required] string? text,
+ [FromQuery, Required] string? header,
+ [FromQuery] long? timeoutMs)
+ {
+ var command = new MessageCommand
+ {
+ Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
+ TimeoutMs = timeoutMs,
+ Text = text
+ };
+
+ _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
+
+ 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] 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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult RemoveUserFromSession(
+ [FromRoute] string? sessionId,
+ [FromRoute] Guid userId)
+ {
+ _sessionManager.RemoveAdditionalUser(sessionId, userId);
+ return NoContent();
+ }
+
+ /// <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 ActionResult PostCapabilities(
+ [FromQuery, Required] string? id,
+ [FromQuery] string? playableMediaTypes,
+ [FromQuery] string? supportedCommands,
+ [FromQuery] bool supportsMediaControl = false,
+ [FromQuery] bool supportsSync = false,
+ [FromQuery] bool supportsPersistentIdentifier = true)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ }
+
+ _sessionManager.ReportCapabilities(id, new ClientCapabilities
+ {
+ PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
+ SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true),
+ 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 ActionResult PostFullCapabilities(
+ [FromQuery] string? id,
+ [FromBody, Required] ClientCapabilities capabilities)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ }
+
+ _sessionManager.ReportCapabilities(id, capabilities);
+
+ 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 ActionResult ReportViewing(
+ [FromQuery] string? sessionId,
+ [FromQuery] string? itemId)
+ {
+ string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+
+ _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 ActionResult ReportSessionEnded()
+ {
+ AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request);
+
+ _sessionManager.Logout(auth.Token);
+ 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 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 04f134b8b..c8e3cc4f5 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -5,6 +5,7 @@ using Jellyfin.Api.Models.StartupDtos;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
@@ -30,72 +31,87 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Api endpoint for completing the startup wizard.
+ /// Completes the startup wizard.
/// </summary>
+ /// <response code="204">Startup wizard completed.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Complete")]
- public void CompleteWizard()
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CompleteWizard()
{
_config.Configuration.IsStartupWizardCompleted = true;
_config.SaveConfiguration();
+ return NoContent();
}
/// <summary>
- /// Endpoint for getting the initial startup wizard configuration.
+ /// Gets the initial startup wizard configuration.
/// </summary>
- /// <returns>The initial startup wizard configuration.</returns>
+ /// <response code="200">Initial startup wizard configuration retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
[HttpGet("Configuration")]
- public StartupConfigurationDto GetStartupConfiguration()
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
{
- var result = new StartupConfigurationDto
+ return new StartupConfigurationDto
{
UICulture = _config.Configuration.UICulture,
MetadataCountryCode = _config.Configuration.MetadataCountryCode,
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
};
-
- return result;
}
/// <summary>
- /// Endpoint for updating the initial startup wizard configuration.
+ /// Sets the initial startup wizard configuration.
/// </summary>
/// <param name="uiCulture">The UI language culture.</param>
/// <param name="metadataCountryCode">The metadata country code.</param>
/// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
+ /// <response code="204">Configuration saved.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Configuration")]
- public void UpdateInitialConfiguration(
- [FromForm] string uiCulture,
- [FromForm] string metadataCountryCode,
- [FromForm] string preferredMetadataLanguage)
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateInitialConfiguration(
+ [FromForm] string? uiCulture,
+ [FromForm] string? metadataCountryCode,
+ [FromForm] string? preferredMetadataLanguage)
{
_config.Configuration.UICulture = uiCulture;
_config.Configuration.MetadataCountryCode = metadataCountryCode;
_config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
_config.SaveConfiguration();
+ return NoContent();
}
/// <summary>
- /// Endpoint for (dis)allowing remote access and UPnP.
+ /// Sets remote access and UPnP.
/// </summary>
/// <param name="enableRemoteAccess">Enable remote access.</param>
/// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
+ /// <response code="204">Configuration saved.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoteAccess")]
- public void SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
{
_config.Configuration.EnableRemoteAccess = enableRemoteAccess;
_config.Configuration.EnableUPnP = enableAutomaticPortMapping;
_config.SaveConfiguration();
+ return NoContent();
}
/// <summary>
- /// Endpoint for returning the first user.
+ /// Gets the first user.
/// </summary>
+ /// <response code="200">Initial user retrieved.</response>
/// <returns>The first user.</returns>
[HttpGet("User")]
- public StartupUserDto GetFirstUser()
+ [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.
- _userManager.Initialize();
+ await _userManager.InitializeAsync().ConfigureAwait(false);
var user = _userManager.Users.First();
return new StartupUserDto
{
@@ -105,12 +121,17 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Endpoint for updating the user name and password.
+ /// Sets the user name and password.
/// </summary>
/// <param name="startupUserDto">The DTO containing username and password.</param>
- /// <returns>The async task.</returns>
+ /// <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")]
- public async Task UpdateUser([FromForm] StartupUserDto startupUserDto)
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> UpdateStartupUser([FromForm] StartupUserDto startupUserDto)
{
var user = _userManager.Users.First();
@@ -122,6 +143,8 @@ namespace Jellyfin.Api.Controllers
{
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
}
+
+ return NoContent();
}
}
}
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
new file mode 100644
index 000000000..6f2787d93
--- /dev/null
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Studios controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class StudiosController : BaseJellyfinApiController
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _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="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. 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 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. 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="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="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] double? minCommunityRating,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? genres,
+ [FromQuery] string? genreIds,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? studioIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem;
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ user = _userManager.GetUserById(userId.Value);
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+ }
+ else
+ {
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+ }
+
+ var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+ var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+ var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypesArr,
+ IncludeItemTypes = includeItemTypesArr,
+ MediaTypes = mediaTypesArr,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = RequestHelpers.Split(tags, ',', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
+ Genres = RequestHelpers.Split(genres, ',', true),
+ GenreIds = RequestHelpers.GetGuids(genreIds),
+ StudioIds = RequestHelpers.GetGuids(studioIds),
+ Person = person,
+ PersonIds = RequestHelpers.GetGuids(personIds),
+ PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+ Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ if (parentItem is Folder)
+ {
+ query.AncestorIds = new[] { new Guid(parentId) };
+ }
+ else
+ {
+ query.ItemIds = new[] { new Guid(parentId) };
+ }
+ }
+
+ // Studios
+ if (!string.IsNullOrEmpty(studios))
+ {
+ query.StudioIds = studios.Split('|').Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetStudio(i);
+ }
+ catch
+ {
+ return null;
+ }
+ }).Where(i => i != null).Select(i => i!.Id)
+ .ToArray();
+ }
+
+ foreach (var filter in RequestHelpers.GetFilters(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 = new QueryResult<(BaseItem, ItemCounts)>();
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, itemCounts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (!string.IsNullOrWhiteSpace(includeItemTypes))
+ {
+ 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;
+ });
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
+
+ /// <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] string name, [FromQuery] Guid? userId)
+ {
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ var item = _libraryManager.GetStudio(name);
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ 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
new file mode 100644
index 000000000..988acccc3
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -0,0 +1,350 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Subtitle controller.
+ /// </summary>
+ [Route("")]
+ public class SubtitleController : BaseJellyfinApiController
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly ISubtitleManager _subtitleManager;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly IAuthorizationContext _authContext;
+ private readonly ILogger<SubtitleController> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SubtitleController"/> class.
+ /// </summary>
+ /// <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="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
+ public SubtitleController(
+ ILibraryManager libraryManager,
+ ISubtitleManager subtitleManager,
+ ISubtitleEncoder subtitleEncoder,
+ IMediaSourceManager mediaSourceManager,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ IAuthorizationContext authContext,
+ ILogger<SubtitleController> logger)
+ {
+ _libraryManager = libraryManager;
+ _subtitleManager = subtitleManager;
+ _subtitleEncoder = subtitleEncoder;
+ _mediaSourceManager = mediaSourceManager;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ _authContext = authContext;
+ _logger = logger;
+ }
+
+ /// <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] Guid itemId,
+ [FromRoute] int index)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ _subtitleManager.DeleteSubtitles(item, index);
+ return NoContent();
+ }
+
+ /// <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] Guid itemId,
+ [FromRoute, Required] string? language,
+ [FromQuery] bool? isPerfectMatch)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
+
+ return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, 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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> DownloadRemoteSubtitles(
+ [FromRoute] Guid itemId,
+ [FromRoute, Required] string? subtitleId)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
+
+ try
+ {
+ await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error downloading subtitles");
+ }
+
+ return NoContent();
+ }
+
+ /// <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)]
+ public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id)
+ {
+ var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
+
+ return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
+ }
+
+ /// <summary>
+ /// Gets subtitles in a specified format.
+ /// </summary>
+ /// <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">Optional. The start position of the subtitle in ticks.</param>
+ /// <response code="200">File returned.</response>
+ /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
+ [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
+ [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult> GetSubtitle(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string? mediaSourceId,
+ [FromRoute, Required] int index,
+ [FromRoute, Required] string? format,
+ [FromQuery] long? endPositionTicks,
+ [FromQuery] bool copyTimestamps = false,
+ [FromQuery] bool addVttTimeMap = false,
+ [FromRoute] long startPositionTicks = 0)
+ {
+ if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
+ {
+ format = "json";
+ }
+
+ if (string.IsNullOrEmpty(format))
+ {
+ var item = (Video)_libraryManager.GetItemById(itemId);
+
+ var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
+ var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
+ .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
+
+ var subtitleStream = mediaSource.MediaStreams
+ .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
+
+ FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read);
+ return File(stream, MimeTypes.GetMimeType(subtitleStream.Path));
+ }
+
+ if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
+ {
+ await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+ using var reader = new StreamReader(stream);
+
+ var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+ 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(
+ await EncodeSubtitles(
+ itemId,
+ mediaSourceId,
+ index,
+ format,
+ startPositionTicks,
+ endPositionTicks,
+ 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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetSubtitlePlaylist(
+ [FromRoute] Guid itemId,
+ [FromRoute] int index,
+ [FromRoute] string? mediaSourceId,
+ [FromQuery, Required] int segmentLength)
+ {
+ var item = (Video)_libraryManager.GetItemById(itemId);
+
+ var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
+
+ var runtime = mediaSource.RunTimeTicks ?? -1;
+
+ if (runtime <= 0)
+ {
+ throw new ArgumentException("HLS Subtitles are not supported for this media.");
+ }
+
+ var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
+ if (segmentLengthTicks <= 0)
+ {
+ throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
+ }
+
+ var builder = new StringBuilder();
+ builder.AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
+ .AppendLine("#EXT-X-VERSION:3")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+
+ long positionTicks = 0;
+
+ var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
+
+ while (positionTicks < runtime)
+ {
+ var remaining = runtime - positionTicks;
+ var lengthTicks = Math.Min(remaining, segmentLengthTicks);
+
+ builder.Append("#EXTINF:")
+ .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
+ .AppendLine(",");
+
+ var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
+
+ var url = string.Format(
+ CultureInfo.CurrentCulture,
+ "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
+ positionTicks.ToString(CultureInfo.InvariantCulture),
+ endPositionTicks.ToString(CultureInfo.InvariantCulture),
+ accessToken);
+
+ builder.AppendLine(url);
+
+ positionTicks += segmentLengthTicks;
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return File(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)
+ {
+ var item = _libraryManager.GetItemById(id);
+
+ return _subtitleEncoder.GetSubtitles(
+ item,
+ mediaSourceId,
+ index,
+ format,
+ startPositionTicks,
+ endPositionTicks ?? 0,
+ copyTimestamps,
+ CancellationToken.None);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
new file mode 100644
index 000000000..42db6b6a1
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The suggestions controller.
+ /// </summary>
+ [Route("")]
+ public class SuggestionsController : BaseJellyfinApiController
+ {
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _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] Guid userId,
+ [FromQuery] string? mediaType,
+ [FromQuery] string? type,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool enableTotalRecordCount = false)
+ {
+ var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
+ {
+ OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+ MediaTypes = RequestHelpers.Split(mediaType!, ',', true),
+ IncludeItemTypes = RequestHelpers.Split(type!, ',', true),
+ IsVirtualItem = false,
+ StartIndex = startIndex,
+ Limit = limit,
+ DtoOptions = dtoOptions,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ Recursive = true
+ });
+
+ var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
+
+ return new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = result.TotalRecordCount,
+ Items = dtoList
+ };
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
new file mode 100644
index 000000000..e16a10ba4
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Threading;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The sync play controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class SyncPlayController : BaseJellyfinApiController
+ {
+ private readonly ISessionManager _sessionManager;
+ private readonly IAuthorizationContext _authorizationContext;
+ private readonly ISyncPlayManager _syncPlayManager;
+
+ /// <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="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
+ public SyncPlayController(
+ ISessionManager sessionManager,
+ IAuthorizationContext authorizationContext,
+ ISyncPlayManager syncPlayManager)
+ {
+ _sessionManager = sessionManager;
+ _authorizationContext = authorizationContext;
+ _syncPlayManager = syncPlayManager;
+ }
+
+ /// <summary>
+ /// Create a new SyncPlay group.
+ /// </summary>
+ /// <response code="204">New group created.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("New")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayCreateGroup()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Join an existing SyncPlay group.
+ /// </summary>
+ /// <param name="groupId">The sync play group id.</param>
+ /// <response code="204">Group join successful.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Join")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+
+ var joinRequest = new JoinGroupRequest()
+ {
+ GroupId = groupId
+ };
+
+ _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, 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)]
+ public ActionResult SyncPlayLeaveGroup()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets all SyncPlay groups.
+ /// </summary>
+ /// <param name="filterItemId">Optional. Filter by item id.</param>
+ /// <response code="200">Groups returned.</response>
+ /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
+ [HttpGet("List")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<GroupInfoView>> SyncPlayGetGroups([FromQuery] Guid? filterItemId)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
+ }
+
+ /// <summary>
+ /// Request play in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Play request sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Play")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayPlay()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.Play
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request pause in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Pause request sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Pause")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayPause()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.Pause
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request seek in SyncPlay group.
+ /// </summary>
+ /// <param name="positionTicks">The playback position in ticks.</param>
+ /// <response code="204">Seek request sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Seek")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySeek([FromQuery] long positionTicks)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.Seek,
+ PositionTicks = positionTicks
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request group wait in SyncPlay group while buffering.
+ /// </summary>
+ /// <param name="when">When the request has been made by the client.</param>
+ /// <param name="positionTicks">The playback position in ticks.</param>
+ /// <param name="bufferingDone">Whether the buffering is done.</param>
+ /// <response code="204">Buffering request sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Buffering")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new PlaybackRequest()
+ {
+ Type = bufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
+ When = when,
+ PositionTicks = positionTicks
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Update session ping.
+ /// </summary>
+ /// <param name="ping">The ping.</param>
+ /// <response code="204">Ping updated.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Ping")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayPing([FromQuery] double ping)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.Ping,
+ Ping = Convert.ToInt64(ping)
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
new file mode 100644
index 000000000..bbfd163de
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -0,0 +1,219 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <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>
+ /// 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;
+ }
+
+ /// <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 async Task<ActionResult<SystemInfo>> GetSystemInfo()
+ {
+ return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <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 async Task<ActionResult<PublicSystemInfo>> GetPublicSystemInfo()
+ {
+ return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
+ }
+
+ /// <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()
+ {
+ Task.Run(async () =>
+ {
+ 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()
+ {
+ Task.Run(async () =>
+ {
+ 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;
+
+ 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;
+ }
+
+ /// <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()
+ {
+ return new EndPointInfo
+ {
+ IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress),
+ IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString())
+ };
+ }
+
+ /// <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)]
+ public ActionResult GetLogFile([FromQuery, Required] string? name)
+ {
+ var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
+ .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+
+ // For older files, assume fully static
+ var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
+
+ FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
+ return File(stream, "text/plain");
+ }
+
+ /// <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)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
+ {
+ var result = _appHost.GetWakeOnLanInfo();
+ return Ok(result);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
new file mode 100644
index 000000000..2dc744e7c
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Globalization;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The time sync controller.
+ /// </summary>
+ [Route("")]
+ public class TimeSyncController : BaseJellyfinApiController
+ {
+ /// <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.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
+
+ var response = new UtcTimeResponse();
+ response.RequestReceptionTime = requestReceptionTime;
+
+ // Important to keep the following two lines at the end
+ var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
+ response.ResponseTransmissionTime = responseTransmissionTime;
+
+ // Implementing NTP on such a high level results in this useless
+ // information being sent. On the other hand it enables future additions.
+ return response;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
new file mode 100644
index 000000000..5157b08ae
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -0,0 +1,281 @@
+using System;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The trailers controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class TrailersController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <summary>
+ /// Finds movies and trailers similar to a given trailer.
+ /// </summary>
+ /// <param name="userId">The user id.</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 delimeted.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</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="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</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 delimeted. 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 delimeted.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. 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 delimeted. 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 delimeted.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</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 delimeted.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</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 delimeted.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</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 delimeted.</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 delimeted.</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 delimeted.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</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] string? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery] string? locationTypes,
+ [FromQuery] string? 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] string? excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? sortOrder,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? imageTypes,
+ [FromQuery] string? sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery] string? genres,
+ [FromQuery] string? officialRatings,
+ [FromQuery] string? tags,
+ [FromQuery] string? years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery] string? personIds,
+ [FromQuery] string? personTypes,
+ [FromQuery] string? studios,
+ [FromQuery] string? artists,
+ [FromQuery] string? excludeArtistIds,
+ [FromQuery] string? artistIds,
+ [FromQuery] string? albumArtistIds,
+ [FromQuery] string? contributingArtistIds,
+ [FromQuery] string? albums,
+ [FromQuery] string? albumIds,
+ [FromQuery] string? ids,
+ [FromQuery] string? 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] string? seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] string? studioIds,
+ [FromQuery] string? genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ var includeItemTypes = "Trailer";
+
+ return _itemsController
+ .GetItems(
+ userId,
+ 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,
+ 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
new file mode 100644
index 000000000..f463ab889
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -0,0 +1,385 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.TV;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The tv shows controller.
+ /// </summary>
+ [Route("Shows")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class TvShowsController : BaseJellyfinApiController
+ {
+ 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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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="enableImges">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="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+ [HttpGet("NextUp")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
+ [FromQuery, Required] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] string? seriesId,
+ [FromQuery] string? parentId,
+ [FromQuery] bool? enableImges,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var options = new DtoOptions()
+ .AddItemFields(fields!)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+
+ var result = _tvSeriesManager.GetNextUp(
+ new NextUpQuery
+ {
+ Limit = limit,
+ ParentId = parentId,
+ SeriesId = seriesId,
+ StartIndex = startIndex,
+ UserId = userId ?? Guid.Empty,
+ EnableTotalRecordCount = enableTotalRecordCount
+ },
+ options);
+
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
+
+ return new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = result.TotalRecordCount,
+ Items = 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. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</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="enableImges">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, Required] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] string? parentId,
+ [FromQuery] bool? enableImges,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] bool? enableUserData)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
+
+ var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+
+ var options = new DtoOptions()
+ .AddItemFields(fields!)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+
+ var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { nameof(Episode) },
+ OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ MinPremiereDate = minPremiereDate,
+ StartIndex = startIndex,
+ Limit = limit,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ DtoOptions = options
+ });
+
+ var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
+
+ return new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = itemsResult.Count,
+ Items = returnItems
+ };
+ }
+
+ /// <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 delimeted. 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 delimeted. 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] string? seriesId,
+ [FromQuery, Required] Guid? userId,
+ [FromQuery] string? fields,
+ [FromQuery] int? season,
+ [FromQuery] string? seasonId,
+ [FromQuery] bool? isMissing,
+ [FromQuery] string? adjacentTo,
+ [FromQuery] string? startItemId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] string? sortBy)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ List<BaseItem> episodes;
+
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields!)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+
+ if (!string.IsNullOrWhiteSpace(seasonId)) // Season id was supplied. Get episodes by season id.
+ {
+ var item = _libraryManager.GetItemById(new Guid(seasonId));
+ if (!(item is Season seasonItem))
+ {
+ return NotFound("No season exists with Id " + seasonId);
+ }
+
+ episodes = seasonItem.GetEpisodes(user, dtoOptions);
+ }
+ else if (season.HasValue) // Season number was supplied. Get episodes by season number
+ {
+ if (!(_libraryManager.GetItemById(seriesId) is Series series))
+ {
+ return NotFound("Series not found");
+ }
+
+ var seasonItem = series
+ .GetSeasons(user, dtoOptions)
+ .FirstOrDefault(i => i.IndexNumber == season.Value);
+
+ episodes = seasonItem == 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 Series series))
+ {
+ return NotFound("Series not found");
+ }
+
+ episodes = series.GetEpisodes(user, dtoOptions).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();
+ }
+
+ if (!string.IsNullOrWhiteSpace(startItemId))
+ {
+ episodes = episodes
+ .SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), startItemId, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ }
+
+ // This must be the last filter
+ if (!string.IsNullOrEmpty(adjacentTo))
+ {
+ episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList();
+ }
+
+ if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
+ {
+ episodes.Shuffle();
+ }
+
+ var returnItems = episodes;
+
+ if (startIndex.HasValue || limit.HasValue)
+ {
+ returnItems = ApplyPaging(episodes, startIndex, limit).ToList();
+ }
+
+ var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
+
+ return new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = episodes.Count,
+ Items = dtos
+ };
+ }
+
+ /// <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 delimeted. 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] string? seriesId,
+ [FromQuery, Required] Guid? userId,
+ [FromQuery] string? fields,
+ [FromQuery] bool? isSpecialSeason,
+ [FromQuery] bool? isMissing,
+ [FromQuery] string? adjacentTo,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] bool? enableUserData)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ if (!(_libraryManager.GetItemById(seriesId) is Series series))
+ {
+ return NotFound("Series not found");
+ }
+
+ var seasons = series.GetItemList(new InternalItemsQuery(user)
+ {
+ IsMissing = isMissing,
+ IsSpecialSeason = isSpecialSeason,
+ AdjacentTo = adjacentTo
+ });
+
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+
+ var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
+
+ return new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = returnItems.Count,
+ Items = 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
new file mode 100644
index 000000000..b13cf9fa5
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The universal audio controller.
+ /// </summary>
+ [Route("")]
+ public class UniversalAudioController : BaseJellyfinApiController
+ {
+ private readonly IAuthorizationContext _authorizationContext;
+ private readonly IDeviceManager _deviceManager;
+ 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="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> 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{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(
+ IAuthorizationContext authorizationContext,
+ IDeviceManager deviceManager,
+ ILibraryManager libraryManager,
+ ILogger<UniversalAudioController> logger,
+ MediaInfoHelper mediaInfoHelper,
+ AudioHelper audioHelper,
+ DynamicHlsHelper dynamicHlsHelper)
+ {
+ _authorizationContext = authorizationContext;
+ _deviceManager = deviceManager;
+ _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="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")]
+ [HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
+ [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
+ [HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status302Found)]
+ public async Task<ActionResult> GetUniversalAudioStream(
+ [FromRoute] Guid itemId,
+ [FromRoute] string? container,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] int? transcodingAudioChannels,
+ [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] string? transcodingContainer,
+ [FromQuery] string? transcodingProtocol,
+ [FromQuery] int? maxAudioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] bool? enableRemoteMedia,
+ [FromQuery] bool breakOnNonKeyFrames,
+ [FromQuery] bool enableRedirection = true)
+ {
+ var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
+ _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
+
+ var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
+
+ _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
+
+ if (deviceProfile == null)
+ {
+ var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
+ if (clientCapabilities != null)
+ {
+ deviceProfile = clientCapabilities.DeviceProfile;
+ }
+ }
+
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId)
+ .ConfigureAwait(false);
+
+ if (deviceProfile != null)
+ {
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
+
+ foreach (var sourceInfo in info.MediaSources)
+ {
+ _mediaInfoHelper.SetDeviceSpecificData(
+ item,
+ sourceInfo,
+ deviceProfile,
+ authInfo,
+ maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+ startTimeTicks ?? 0,
+ mediaSourceId ?? string.Empty,
+ null,
+ null,
+ maxAudioChannels,
+ info!.PlaySessionId!,
+ userId ?? Guid.Empty,
+ true,
+ true,
+ true,
+ true,
+ true,
+ Request.HttpContext.Connection.RemoteIpAddress.ToString());
+ }
+
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+ }
+
+ if (info.MediaSources != null)
+ {
+ foreach (var source in info.MediaSources)
+ {
+ _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
+ }
+ }
+
+ var mediaSource = info.MediaSources![0];
+ if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
+ {
+ if (enableRedirection)
+ {
+ if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
+ {
+ return Redirect(mediaSource.Path);
+ }
+ }
+ }
+
+ 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
+ // TODO: remove this when we switch back to the segment muxer
+ var supportedHlsContainers = new[] { "mpegts", "fmp4" };
+
+ 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 : "mpegts",
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = true,
+ AllowAudioStreamCopy = true,
+ AllowVideoStreamCopy = true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames,
+ AudioSampleRate = maxAudioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+ StartTimeTicks = startTimeTicks,
+ SubtitleMethod = SubtitleDeliveryMethod.Hls,
+ RequireAvc = true,
+ DeInterlace = true,
+ RequireNonAnamorphic = true,
+ EnableMpegtsM2TsMode = true,
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ Context = EncodingContext.Static,
+ StreamOptions = new Dictionary<string, string>(),
+ EnableAdaptiveBitrateStreaming = true
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
+ .ConfigureAwait(false);
+ }
+
+ var audioStreamingDto = new StreamingRequestDto
+ {
+ 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 ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = maxAudioChannels,
+ CopyTimestamps = true,
+ StartTimeTicks = startTimeTicks,
+ SubtitleMethod = SubtitleDeliveryMethod.Embed,
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ Context = EncodingContext.Static
+ };
+
+ return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
+ }
+
+ private DeviceProfile GetDeviceProfile(
+ string? container,
+ string? transcodingContainer,
+ string? audioCodec,
+ string? transcodingProtocol,
+ bool? breakOnNonKeyFrames,
+ int? transcodingAudioChannels,
+ int? maxAudioSampleRate,
+ int? maxAudioBitDepth,
+ int? maxAudioChannels)
+ {
+ var deviceProfile = new DeviceProfile();
+
+ var directPlayProfiles = new List<DirectPlayProfile>();
+
+ var containers = RequestHelpers.Split(container, ',', true);
+
+ foreach (var cont in containers)
+ {
+ var parts = RequestHelpers.Split(cont, ',', true);
+
+ var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
+
+ directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
+ }
+
+ deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
+
+ deviceProfile.TranscodingProfiles = new[]
+ {
+ new TranscodingProfile
+ {
+ Type = DlnaProfileType.Audio,
+ Context = EncodingContext.Streaming,
+ Container = transcodingContainer,
+ AudioCodec = audioCodec,
+ Protocol = transcodingProtocol,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
+ }
+ };
+
+ var codecProfiles = new List<CodecProfile>();
+ var conditions = new List<ProfileCondition>();
+
+ if (maxAudioSampleRate.HasValue)
+ {
+ // codec profile
+ conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioSampleRate, Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) });
+ }
+
+ if (maxAudioBitDepth.HasValue)
+ {
+ // codec profile
+ conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioBitDepth, Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) });
+ }
+
+ if (maxAudioChannels.HasValue)
+ {
+ // codec profile
+ conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioChannels, Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) });
+ }
+
+ if (conditions.Count > 0)
+ {
+ // codec profile
+ codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() });
+ }
+
+ deviceProfile.CodecProfiles = codecProfiles.ToArray();
+
+ return deviceProfile;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
new file mode 100644
index 000000000..272312522
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -0,0 +1,541 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.UserDtos;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Users;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+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;
+
+ /// <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>
+ public UserController(
+ IUserManager userManager,
+ ISessionManager sessionManager,
+ INetworkManager networkManager,
+ IDeviceManager deviceManager,
+ IAuthorizationContext authContext,
+ IServerConfigurationManager config)
+ {
+ _userManager = userManager;
+ _sessionManager = sessionManager;
+ _networkManager = networkManager;
+ _deviceManager = deviceManager;
+ _authContext = authContext;
+ _config = config;
+ }
+
+ /// <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));
+ }
+
+ 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] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ if (user == null)
+ {
+ return NotFound("User not found");
+ }
+
+ var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString());
+ return result;
+ }
+
+ /// <summary>
+ /// Deletes a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">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 ActionResult DeleteUser([FromRoute] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ _sessionManager.RevokeUserTokens(user.Id, null);
+ _userManager.DeleteUser(userId);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Authenticates a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="pw">The password as plain text.</param>
+ /// <param name="password">The password sha1-hash.</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)]
+ public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
+ [FromRoute, Required] Guid userId,
+ [FromQuery, Required] string? pw,
+ [FromQuery] string? password)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ if (user == null)
+ {
+ return NotFound("User not found");
+ }
+
+ if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
+ {
+ return Forbid("Only sha1 password is not allowed.");
+ }
+
+ // Password should always be null
+ AuthenticateUserByName request = new AuthenticateUserByName
+ {
+ Username = user.Username,
+ Password = null,
+ Pw = pw
+ };
+ return await AuthenticateUserByName(request).ConfigureAwait(false);
+ }
+
+ /// <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 = _authContext.GetAuthorizationInfo(Request);
+
+ try
+ {
+ var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest
+ {
+ App = auth.Client,
+ AppVersion = auth.Version,
+ DeviceId = auth.DeviceId,
+ DeviceName = auth.Device,
+ Password = request.Pw,
+ PasswordSha1 = request.Password,
+ RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(),
+ Username = request.Username
+ }).ConfigureAwait(false);
+
+ return result;
+ }
+ catch (SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {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="200">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] Guid userId,
+ [FromBody] UpdateUserPassword request)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return Forbid("User is not allowed to update the password.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+
+ if (user == null)
+ {
+ return NotFound("User not found");
+ }
+
+ if (request.ResetPassword)
+ {
+ await _userManager.ResetPassword(user).ConfigureAwait(false);
+ }
+ else
+ {
+ var success = await _userManager.AuthenticateUser(
+ user.Username,
+ request.CurrentPw,
+ request.CurrentPw,
+ HttpContext.Connection.RemoteIpAddress.ToString(),
+ false).ConfigureAwait(false);
+
+ if (success == null)
+ {
+ return Forbid("Invalid user or password entered.");
+ }
+
+ await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
+
+ var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
+
+ _sessionManager.RevokeUserTokens(user.Id, currentToken);
+ }
+
+ 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="200">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 ActionResult UpdateUserEasyPassword(
+ [FromRoute] Guid userId,
+ [FromBody] UpdateUserEasyPassword request)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return Forbid("User is not allowed to update the easy password.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+
+ if (user == null)
+ {
+ return NotFound("User not found");
+ }
+
+ if (request.ResetPassword)
+ {
+ _userManager.ResetEasyPassword(user);
+ }
+ else
+ {
+ _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
+ }
+
+ 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] Guid userId,
+ [FromBody] UserDto updateUser)
+ {
+ if (updateUser == null)
+ {
+ return BadRequest();
+ }
+
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
+ {
+ return Forbid("User update not allowed.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+
+ if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
+ {
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ _userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
+ }
+ else
+ {
+ await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
+ _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
+ }
+
+ return NoContent();
+ }
+
+ /// <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 ActionResult UpdateUserPolicy(
+ [FromRoute] Guid userId,
+ [FromBody] UserPolicy newPolicy)
+ {
+ if (newPolicy == null)
+ {
+ return BadRequest();
+ }
+
+ 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 Forbid("There must be at least one user in the system with administrative access.");
+ }
+ }
+
+ // If disabling
+ if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
+ {
+ return Forbid("Administrators cannot be disabled.");
+ }
+
+ // If disabling
+ if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
+ {
+ if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
+ {
+ return Forbid("There must be at least one enabled user in the system.");
+ }
+
+ var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
+ _sessionManager.RevokeUserTokens(user.Id, currentToken);
+ }
+
+ _userManager.UpdatePolicy(userId, newPolicy);
+
+ 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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public ActionResult UpdateUserConfiguration(
+ [FromRoute] Guid userId,
+ [FromBody] UserConfiguration userConfig)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
+ {
+ return Forbid("User configuration update not allowed");
+ }
+
+ _userManager.UpdateConfiguration(userId, userConfig);
+
+ 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] CreateUserByName request)
+ {
+ var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
+
+ // no need to authenticate password for new user
+ if (request.Password != null)
+ {
+ await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
+ }
+
+ var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString());
+
+ return result;
+ }
+
+ /// <summary>
+ /// Initiates the forgot password process for a local user.
+ /// </summary>
+ /// <param name="enteredUsername">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] string? enteredUsername)
+ {
+ var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress)
+ || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString());
+
+ var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false);
+
+ return result;
+ }
+
+ /// <summary>
+ /// Redeems a forgot password pin.
+ /// </summary>
+ /// <param name="pin">The pin.</param>
+ /// <response code="200">Pin reset process started.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
+ [HttpPost("ForgotPassword/Pin")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin)
+ {
+ var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
+ return result;
+ }
+
+ private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
+ {
+ var users = _userManager.Users;
+
+ if (isDisabled.HasValue)
+ {
+ users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value);
+ }
+
+ if (isHidden.HasValue)
+ {
+ users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value);
+ }
+
+ if (filterByDevice)
+ {
+ var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
+
+ if (!string.IsNullOrWhiteSpace(deviceId))
+ {
+ users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
+ }
+ }
+
+ if (filterByNetwork)
+ {
+ if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()))
+ {
+ users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
+ }
+ }
+
+ var result = users
+ .OrderBy(u => u.Username)
+ .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString()));
+
+ return result;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
new file mode 100644
index 000000000..f55ff6f3d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -0,0 +1,392 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// User library controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ 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;
+
+ /// <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>
+ public UserLibraryController(
+ IUserManager userManager,
+ IUserDataManager userDataRepository,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IUserViewManager userViewManager,
+ IFileSystem fileSystem)
+ {
+ _userManager = userManager;
+ _userDataRepository = userDataRepository;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _userViewManager = userViewManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <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] Guid userId, [FromRoute] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ var item = itemId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ 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] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ var item = _libraryManager.GetUserRootFolder();
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ 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] Guid userId, [FromRoute] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ var item = itemId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos,
+ TotalRecordCount = dtos.Length
+ };
+ }
+
+ /// <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] Guid userId, [FromRoute] 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] Guid userId, [FromRoute] 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] Guid userId, [FromRoute] 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] Guid userId, [FromRoute] 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] Guid userId, [FromRoute] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ var item = itemId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer })
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+ .ToArray();
+
+ if (item is IHasTrailers hasTrailers)
+ {
+ var trailers = hasTrailers.GetTrailers();
+ var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
+ var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
+ dtosExtras.CopyTo(allTrailers, 0);
+ dtosTrailers.CopyTo(allTrailers, dtosExtras.Length);
+ return allTrailers;
+ }
+
+ return dtosExtras;
+ }
+
+ /// <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] Guid userId, [FromRoute] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ var item = itemId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+ return Ok(item
+ .GetExtras(BaseItem.DisplayExtraTypes)
+ .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. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</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] Guid userId,
+ [FromQuery] Guid? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] bool? isPlayed,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? 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;
+ }
+ }
+
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ var list = _userViewManager.GetLatestItems(
+ new LatestItemsQuery
+ {
+ GroupItems = groupItems,
+ IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ 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 != null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
+ {
+ item = i.Item1;
+ childCount = i.Item2.Count;
+ }
+
+ 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)
+ {
+ 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="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)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
+
+ // 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="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 user = _userManager.GetUserById(userId);
+
+ var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
+
+ // 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);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
new file mode 100644
index 000000000..6df7cc779
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.UserViewDtos;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// User views controller.
+ /// </summary>
+ [Route("")]
+ public class UserViewsController : BaseJellyfinApiController
+ {
+ private readonly IUserManager _userManager;
+ private readonly IUserViewManager _userViewManager;
+ private readonly IDtoService _dtoService;
+ private readonly IAuthorizationContext _authContext;
+ private readonly ILibraryManager _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="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public UserViewsController(
+ IUserManager userManager,
+ IUserViewManager userViewManager,
+ IDtoService dtoService,
+ IAuthorizationContext authContext,
+ ILibraryManager libraryManager)
+ {
+ _userManager = userManager;
+ _userViewManager = userViewManager;
+ _dtoService = dtoService;
+ _authContext = authContext;
+ _libraryManager = 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 ActionResult<QueryResult<BaseItemDto>> GetUserViews(
+ [FromRoute] Guid userId,
+ [FromQuery] bool? includeExternalContent,
+ [FromQuery] string? presetViews,
+ [FromQuery] bool includeHidden = false)
+ {
+ var query = new UserViewQuery
+ {
+ UserId = userId,
+ IncludeHidden = includeHidden
+ };
+
+ if (includeExternalContent.HasValue)
+ {
+ query.IncludeExternalContent = includeExternalContent.Value;
+ }
+
+ if (!string.IsNullOrWhiteSpace(presetViews))
+ {
+ query.PresetViews = RequestHelpers.Split(presetViews, ',', true);
+ }
+
+ var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
+ if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
+ }
+
+ var folders = _userViewManager.GetUserViews(query);
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var fields = dtoOptions.Fields.ToList();
+
+ fields.Add(ItemFields.PrimaryImageAspectRatio);
+ fields.Add(ItemFields.DisplayPreferencesId);
+ fields.Remove(ItemFields.BasicSyncInfo);
+ dtoOptions.Fields = fields.ToArray();
+
+ var user = _userManager.GetUserById(userId);
+
+ var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
+ .ToArray();
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos,
+ TotalRecordCount = dtos.Length
+ };
+ }
+
+ /// <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] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user == null)
+ {
+ 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));
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
new file mode 100644
index 000000000..09a1c93e6
--- /dev/null
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -0,0 +1,81 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Attachments controller.
+ /// </summary>
+ [Route("Videos")]
+ public class VideoAttachmentsController : BaseJellyfinApiController
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IAttachmentExtractor _attachmentExtractor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+ public VideoAttachmentsController(
+ ILibraryManager libraryManager,
+ IAttachmentExtractor attachmentExtractor)
+ {
+ _libraryManager = libraryManager;
+ _attachmentExtractor = attachmentExtractor;
+ }
+
+ /// <summary>
+ /// Get video attachment.
+ /// </summary>
+ /// <param name="videoId">Video ID.</param>
+ /// <param name="mediaSourceId">Media Source ID.</param>
+ /// <param name="index">Attachment Index.</param>
+ /// <response code="200">Attachment retrieved.</response>
+ /// <response code="404">Video or attachment not found.</response>
+ /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
+ [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
+ [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<FileStreamResult>> GetAttachment(
+ [FromRoute, Required] Guid videoId,
+ [FromRoute, Required] string mediaSourceId,
+ [FromRoute, Required] int index)
+ {
+ try
+ {
+ var item = _libraryManager.GetItemById(videoId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ var (attachment, stream) = await _attachmentExtractor.GetAttachment(
+ item,
+ mediaSourceId,
+ index,
+ CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var contentType = string.IsNullOrWhiteSpace(attachment.MimeType)
+ ? MediaTypeNames.Application.Octet
+ : attachment.MimeType;
+
+ return new FileStreamResult(stream, contentType);
+ }
+ catch (ResourceNotFoundException e)
+ {
+ return NotFound(e.Message);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
new file mode 100644
index 000000000..76188f46d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -0,0 +1,503 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaybackDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The video hls controller.
+ /// </summary>
+ [Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class VideoHlsController : BaseJellyfinApiController
+ {
+ private const string DefaultEncoderPreset = "superfast";
+ private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+
+ private readonly EncodingHelper _encodingHelper;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IAuthorizationContext _authContext;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+ private readonly IConfiguration _configuration;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ILogger<VideoHlsController> _logger;
+ private readonly EncodingOptions _encodingOptions;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoHlsController"/> class.
+ /// </summary>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ /// <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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{VideoHlsController}"/>.</param>
+ public VideoHlsController(
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ ISubtitleEncoder subtitleEncoder,
+ IConfiguration configuration,
+ IDlnaManager dlnaManager,
+ IUserManager userManger,
+ IAuthorizationContext authorizationContext,
+ ILibraryManager libraryManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ ILogger<VideoHlsController> logger)
+ {
+ _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+
+ _dlnaManager = dlnaManager;
+ _authContext = authorizationContext;
+ _userManager = userManger;
+ _libraryManager = libraryManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _subtitleEncoder = subtitleEncoder;
+ _configuration = configuration;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _logger = logger;
+ _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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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)]
+ public async Task<ActionResult> GetLiveHlsStream(
+ [FromRoute] 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? transcodingReasons,
+ [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
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ StreamOptions = streamOptions,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
+ };
+
+ var cancellationTokenSource = new CancellationTokenSource();
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ Request,
+ _authContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _fileSystem,
+ _subtitleEncoder,
+ _configuration,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ TranscodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ TranscodingJobDto? job = null;
+ var playlist = state.OutputFilePath;
+
+ if (!System.IO.File.Exists(playlist))
+ {
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ try
+ {
+ if (!System.IO.File.Exists(playlist))
+ {
+ // If the playlist doesn't already exist, startup ffmpeg
+ try
+ {
+ job = await _transcodingJobHelper.StartFfMpeg(
+ state,
+ playlist,
+ GetCommandLineArguments(playlist, state),
+ Request,
+ TranscodingJobType,
+ cancellationTokenSource)
+ .ConfigureAwait(false);
+ job.IsLiveOutput = true;
+ }
+ catch
+ {
+ state.Dispose();
+ throw;
+ }
+
+ minSegments = state.MinSegments;
+ if (minSegments > 0)
+ {
+ await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ }
+ }
+ finally
+ {
+ transcodingLock.Release();
+ }
+ }
+
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType);
+
+ if (job != null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(job);
+ }
+
+ var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength);
+
+ return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
+ }
+
+ /// <summary>
+ /// Gets the command line arguments for ffmpeg.
+ /// </summary>
+ /// <param name="outputPath">The output path of the file.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The command line arguments as a string.</returns>
+ private string GetCommandLineArguments(string outputPath, StreamState state)
+ {
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
+ var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
+ var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
+ var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
+
+ var segmentFormat = format.TrimStart('.');
+ if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ segmentFormat = "mpegts";
+ }
+
+ var baseUrlParam = string.Format(
+ CultureInfo.InvariantCulture,
+ "\"hls{0}\"",
+ Path.GetFileNameWithoutExtension(outputPath));
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"",
+ inputModifier,
+ _encodingHelper.GetInputArgument(state, _encodingOptions),
+ threads,
+ _encodingHelper.GetMapArgs(state),
+ GetVideoArguments(state),
+ GetAudioArguments(state),
+ state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+ string.Empty,
+ segmentFormat,
+ baseUrlParam,
+ outputPath,
+ outputTsArg)
+ .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)
+ {
+ var codec = _encodingHelper.GetAudioEncoder(state);
+
+ if (EncodingHelper.IsCopyCodec(codec))
+ {
+ return "-codec:a:0 copy";
+ }
+
+ var args = "-codec:a:0 " + codec;
+
+ var channels = state.OutputAudioChannels;
+
+ if (channels.HasValue)
+ {
+ args += " -ac " + channels.Value;
+ }
+
+ var bitrate = state.OutputAudioBitrate;
+
+ if (bitrate.HasValue)
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
+
+ return args;
+ }
+
+ /// <summary>
+ /// Gets the video arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The command line arguments for video transcoding.</returns>
+ private string GetVideoArguments(StreamState state)
+ {
+ if (!state.IsOutputVideo)
+ {
+ return string.Empty;
+ }
+
+ var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+
+ var args = "-codec:v:0 " + codec;
+
+ // if (state.EnableMpegtsM2TsMode)
+ // {
+ // args += " -mpegts_m2ts_mode 1";
+ // }
+
+ // See if we can save come cpu cycles by avoiding encoding
+ if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+ {
+ // if h264_mp4toannexb is ever added, do not use it for live tv
+ if (state.VideoStream != null &&
+ !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+ if (!string.IsNullOrEmpty(bitStreamArgs))
+ {
+ args += " " + bitStreamArgs;
+ }
+ }
+ }
+ else
+ {
+ var keyFrameArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -force_key_frames \"expr:gte(t,n_forced*{0})\"",
+ state.SegmentLength.ToString(CultureInfo.InvariantCulture));
+
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
+ args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
+
+ // Add resolution params, if specified
+ if (!hasGraphicalSubs)
+ {
+ args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
+ }
+
+ // This is for internal graphical subs
+ if (hasGraphicalSubs)
+ {
+ args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
+ }
+ }
+
+ args += " -flags -global_header";
+
+ if (!string.IsNullOrEmpty(state.OutputVideoSync))
+ {
+ args += " -vsync " + state.OutputVideoSync;
+ }
+
+ args += _encodingHelper.GetOutputFFlags(state);
+
+ return args;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
new file mode 100644
index 000000000..14d3f2460
--- /dev/null
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -0,0 +1,527 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+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;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+
+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 IAuthorizationContext _authContext;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+ private readonly IConfiguration _configuration;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ private readonly 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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+ public VideosController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService,
+ IDlnaManager dlnaManager,
+ IAuthorizationContext authContext,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ ISubtitleEncoder subtitleEncoder,
+ IConfiguration configuration,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ IHttpClientFactory httpClientFactory)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _dlnaManager = dlnaManager;
+ _authContext = authContext;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _subtitleEncoder = subtitleEncoder;
+ _configuration = configuration;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _httpClientFactory = httpClientFactory;
+ }
+
+ /// <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] Guid itemId, [FromQuery] Guid? userId)
+ {
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ var item = itemId.Equals(Guid.Empty)
+ ? (!userId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.RootFolder)
+ : _libraryManager.GetItemById(itemId);
+
+ var dtoOptions = new DtoOptions();
+ dtoOptions = dtoOptions.AddClientFields(Request);
+
+ BaseItemDto[] items;
+ if (item is Video video)
+ {
+ items = video.GetAdditionalParts()
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
+ .ToArray();
+ }
+ else
+ {
+ items = Array.Empty<BaseItemDto>();
+ }
+
+ var result = new QueryResult<BaseItemDto>
+ {
+ Items = items,
+ TotalRecordCount = items.Length
+ };
+
+ return result;
+ }
+
+ /// <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.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult DeleteAlternateSources([FromRoute] Guid itemId)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
+
+ if (video == null)
+ {
+ return NotFound("The video either does not exist or the id does not belong to a video.");
+ }
+
+ if (video.LinkedAlternateVersions.Length == 0)
+ {
+ video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId);
+ }
+
+ foreach (var link in video.GetLinkedAlternateVersions())
+ {
+ link.SetPrimaryVersionId(null);
+ link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+
+ link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ }
+
+ video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ video.SetPrimaryVersionId(null);
+ video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Merges videos into a single record.
+ /// </summary>
+ /// <param name="itemIds">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 ActionResult MergeVersions([FromQuery, Required] string? itemIds)
+ {
+ var items = RequestHelpers.Split(itemIds, ',', true)
+ .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 videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
+
+ var primaryVersion = videosWithVersions.FirstOrDefault();
+ if (primaryVersion == null)
+ {
+ primaryVersion = items
+ .OrderBy(i =>
+ {
+ if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
+ {
+ return 1;
+ }
+
+ return 0;
+ })
+ .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0)
+ .First();
+ }
+
+ var list = primaryVersion.LinkedAlternateVersions.ToList();
+
+ foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
+ {
+ item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
+
+ item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+
+ list.Add(new LinkedChild
+ {
+ Path = item.Path,
+ ItemId = item.Id
+ });
+
+ foreach (var linkedItem in item.LinkedAlternateVersions)
+ {
+ if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
+ {
+ list.Add(linkedItem);
+ }
+ }
+
+ if (item.LinkedAlternateVersions.Length > 0)
+ {
+ item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ }
+ }
+
+ primaryVersion.LinkedAlternateVersions = list.ToArray();
+ primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ 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 lenght.</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, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodingReasons">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=stream}.{container?}", Name = "GetVideoStream_2")]
+ [HttpGet("{itemId}/stream")]
+ [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
+ [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult> GetVideoStream(
+ [FromRoute] Guid itemId,
+ [FromRoute] 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? transcodingReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+ var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new VideoRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ 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 ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodingReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context,
+ StreamOptions = streamOptions
+ };
+
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ Request,
+ _authContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _fileSystem,
+ _subtitleEncoder,
+ _configuration,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ _transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+
+ await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ }.WriteToAsync(Response.Body, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+ return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
+ }
+
+ // Static remote stream
+ if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+
+ var httpClient = _httpClientFactory.CreateClient();
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
+ }
+
+ if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
+ {
+ return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
+ }
+
+ var outputPath = state.OutputFilePath;
+ var outputPathExists = System.IO.File.Exists(outputPath);
+
+ var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+ var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
+
+ // Static stream
+ if (@static.HasValue && @static.Value)
+ {
+ var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+ if (state.MediaSource.IsInfiniteStream)
+ {
+ await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ }.WriteToAsync(Response.Body, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ return File(Response.Body, contentType);
+ }
+
+ return FileStreamResponseHelpers.GetStaticFileResult(
+ state.MediaPath,
+ contentType,
+ isHeadRequest,
+ HttpContext);
+ }
+
+ // Need to start ffmpeg (because media can't be returned directly)
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+ var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
+ return await FileStreamResponseHelpers.GetTranscodedFile(
+ state,
+ isHeadRequest,
+ HttpContext,
+ _transcodingJobHelper,
+ ffmpegCommandLineArguments,
+ _transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
new file mode 100644
index 000000000..eb91ac23e
--- /dev/null
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -0,0 +1,234 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Years controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class YearsController : BaseJellyfinApiController
+ {
+ 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;
+ }
+
+ /// <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. 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 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] string? sortOrder,
+ [FromQuery] string? parentId,
+ [FromQuery] string? fields,
+ [FromQuery] string? excludeItemTypes,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] string? sortBy,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool recursive = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ var dtoOptions = new DtoOptions()
+ .AddItemFields(fields)
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem;
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ user = _userManager.GetUserById(userId.Value);
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+ }
+ else
+ {
+ parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+ }
+
+ IList<BaseItem> items;
+
+ var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+ var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+ var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypesArr,
+ IncludeItemTypes = includeItemTypesArr,
+ MediaTypes = mediaTypesArr,
+ DtoOptions = dtoOptions
+ };
+
+ bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr);
+
+ if (parentItem.IsFolder)
+ {
+ var folder = (Folder)parentItem;
+
+ if (!userId.Equals(Guid.Empty))
+ {
+ items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
+ }
+ else
+ {
+ items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
+ }
+ }
+ else
+ {
+ items = new[] { parentItem }.Where(Filter).ToList();
+ }
+
+ var extractedItems = GetAllItems(items);
+
+ var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder));
+
+ var ibnItemsArray = filteredItems.ToList();
+
+ IEnumerable<BaseItem> ibnItems = ibnItemsArray;
+
+ var result = new QueryResult<BaseItemDto> { TotalRecordCount = ibnItemsArray.Count };
+
+ if (startIndex.HasValue || limit.HasValue)
+ {
+ if (startIndex.HasValue)
+ {
+ ibnItems = ibnItems.Skip(startIndex.Value);
+ }
+
+ if (limit.HasValue)
+ {
+ ibnItems = ibnItems.Take(limit.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));
+
+ result.Items = dtos.Where(i => i != 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] int year, [FromQuery] Guid? userId)
+ {
+ var item = _libraryManager.GetYear(year);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(Request);
+
+ if (userId.HasValue && !userId.Equals(Guid.Empty))
+ {
+ var user = _userManager.GetUserById(userId.Value);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
+ }
+
+ private bool FilterItem(BaseItem f, IReadOnlyCollection<string> excludeItemTypes, IReadOnlyCollection<string> includeItemTypes, IReadOnlyCollection<string> mediaTypes)
+ {
+ // Exclude item types
+ if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Include item types
+ if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Include MediaTypes
+ if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ 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/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
new file mode 100644
index 000000000..e61e9c29d
--- /dev/null
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Extensions
+{
+ /// <summary>
+ /// Dto Extensions.
+ /// </summary>
+ public static class DtoExtensions
+ {
+ /// <summary>
+ /// Add Dto Item fields.
+ /// </summary>
+ /// <remarks>
+ /// Converted from IHasItemFields.
+ /// Legacy order: 1.
+ /// </remarks>
+ /// <param name="dtoOptions">DtoOptions object.</param>
+ /// <param name="fields">Comma delimited string of fields.</param>
+ /// <returns>Modified DtoOptions object.</returns>
+ internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string? fields)
+ {
+ if (string.IsNullOrEmpty(fields))
+ {
+ dtoOptions.Fields = Array.Empty<ItemFields>();
+ }
+ else
+ {
+ dtoOptions.Fields = fields.Split(',')
+ .Select(v =>
+ {
+ if (Enum.TryParse(v, true, out ItemFields value))
+ {
+ return (ItemFields?)value;
+ }
+
+ return null;
+ })
+ .Where(i => i.HasValue)
+ .Select(i => i!.Value)
+ .ToArray();
+ }
+
+ return dtoOptions;
+ }
+
+ /// <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="request">Current request.</param>
+ /// <returns>Modified DtoOptions object.</returns>
+ internal static DtoOptions AddClientFields(
+ this DtoOptions dtoOptions, HttpRequest request)
+ {
+ dtoOptions.Fields ??= Array.Empty<ItemFields>();
+
+ string? client = ClaimHelpers.GetClient(request.HttpContext.User);
+
+ // No client in claim
+ if (string.IsNullOrEmpty(client))
+ {
+ return dtoOptions;
+ }
+
+ if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
+ {
+ if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ int oldLen = dtoOptions.Fields.Length;
+ var arr = new ItemFields[oldLen + 1];
+ dtoOptions.Fields.CopyTo(arr, 0);
+ arr[oldLen] = ItemFields.RecursiveItemCount;
+ dtoOptions.Fields = arr;
+ }
+ }
+
+ if (!dtoOptions.ContainsField(ItemFields.ChildCount))
+ {
+ if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ int oldLen = dtoOptions.Fields.Length;
+ var arr = new ItemFields[oldLen + 1];
+ dtoOptions.Fields.CopyTo(arr, 0);
+ arr[oldLen] = ItemFields.ChildCount;
+ dtoOptions.Fields = arr;
+ }
+ }
+
+ return dtoOptions;
+ }
+
+ /// <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,
+ string? enableImageTypes)
+ {
+ dtoOptions.EnableImages = enableImages ?? true;
+
+ if (imageTypeLimit.HasValue)
+ {
+ dtoOptions.ImageTypeLimit = imageTypeLimit.Value;
+ }
+
+ if (enableUserData.HasValue)
+ {
+ dtoOptions.EnableUserData = enableUserData.Value;
+ }
+
+ if (!string.IsNullOrWhiteSpace(enableImageTypes))
+ {
+ dtoOptions.ImageTypes = enableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
+ .ToArray();
+ }
+
+ return dtoOptions;
+ }
+
+ /// <summary>
+ /// Check if DtoOptions contains field.
+ /// </summary>
+ /// <param name="dtoOptions">DtoOptions object.</param>
+ /// <param name="field">Field to check.</param>
+ /// <returns>Field existence.</returns>
+ internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field)
+ => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field);
+ }
+}
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
new file mode 100644
index 000000000..eae2be05b
--- /dev/null
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -0,0 +1,195 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// Audio helper.
+ /// </summary>
+ public class AudioHelper
+ {
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IAuthorizationContext _authContext;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+ private readonly IConfiguration _configuration;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ /// <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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of <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>
+ public AudioHelper(
+ IDlnaManager dlnaManager,
+ IAuthorizationContext authContext,
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ ISubtitleEncoder subtitleEncoder,
+ IConfiguration configuration,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ IHttpClientFactory httpClientFactory,
+ IHttpContextAccessor httpContextAccessor)
+ {
+ _dlnaManager = dlnaManager;
+ _authContext = authContext;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _subtitleEncoder = subtitleEncoder;
+ _configuration = configuration;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _httpClientFactory = httpClientFactory;
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ /// <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)
+ {
+ bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ _httpContextAccessor.HttpContext.Request,
+ _authContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _fileSystem,
+ _subtitleEncoder,
+ _configuration,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ if (streamingRequest.Static && state.DirectStreamProvider != null)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+
+ await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+ return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!);
+ }
+
+ // 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 httpClient = _httpClientFactory.CreateClient();
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
+ }
+
+ if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
+ {
+ return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically");
+ }
+
+ var outputPath = state.OutputFilePath;
+ var outputPathExists = System.IO.File.Exists(outputPath);
+
+ var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+ var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+ 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)
+ {
+ await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType);
+ }
+
+ return FileStreamResponseHelpers.GetStaticFileResult(
+ state.MediaPath,
+ contentType,
+ isHeadRequest,
+ _httpContextAccessor.HttpContext);
+ }
+
+ // Need to start ffmpeg (because media can't be returned directly)
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+ var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+ return await FileStreamResponseHelpers.GetTranscodedFile(
+ state,
+ isHeadRequest,
+ _httpContextAccessor.HttpContext,
+ _transcodingJobHelper,
+ ffmpegCommandLineArguments,
+ transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
new file mode 100644
index 000000000..df235ced2
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Linq;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// Claim Helpers.
+ /// </summary>
+ public static class ClaimHelpers
+ {
+ /// <summary>
+ /// Get user id from claims.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>User id.</returns>
+ public static Guid? GetUserId(in ClaimsPrincipal user)
+ {
+ var value = GetClaimValue(user, InternalClaimTypes.UserId);
+ return string.IsNullOrEmpty(value)
+ ? null
+ : (Guid?)Guid.Parse(value);
+ }
+
+ /// <summary>
+ /// Get device id from claims.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>Device id.</returns>
+ public static string? GetDeviceId(in ClaimsPrincipal user)
+ => GetClaimValue(user, InternalClaimTypes.DeviceId);
+
+ /// <summary>
+ /// Get device from claims.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>Device.</returns>
+ public static string? GetDevice(in ClaimsPrincipal user)
+ => GetClaimValue(user, InternalClaimTypes.Device);
+
+ /// <summary>
+ /// Get client from claims.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>Client.</returns>
+ public static string? GetClient(in ClaimsPrincipal user)
+ => GetClaimValue(user, InternalClaimTypes.Client);
+
+ /// <summary>
+ /// Get version from claims.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>Version.</returns>
+ public static string? GetVersion(in ClaimsPrincipal user)
+ => GetClaimValue(user, InternalClaimTypes.Version);
+
+ /// <summary>
+ /// Get token from claims.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>Token.</returns>
+ public static string? GetToken(in ClaimsPrincipal user)
+ => GetClaimValue(user, InternalClaimTypes.Token);
+
+ private static string? GetClaimValue(in ClaimsPrincipal user, string name)
+ {
+ return user?.Identities
+ .SelectMany(c => c.Claims)
+ .Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase))
+ .Select(claim => claim.Value)
+ .FirstOrDefault();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
new file mode 100644
index 000000000..6a8829d46
--- /dev/null
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -0,0 +1,550 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Security.Claims;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+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 IAuthorizationContext _authContext;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+ private readonly IConfiguration _configuration;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly INetworkManager _networkManager;
+ private readonly ILogger<DynamicHlsHelper> _logger;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ /// <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="authContext">Instance of the <see cref="IAuthorizationContext"/> 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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of <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>
+ public DynamicHlsHelper(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDlnaManager dlnaManager,
+ IAuthorizationContext authContext,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ ISubtitleEncoder subtitleEncoder,
+ IConfiguration configuration,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ INetworkManager networkManager,
+ ILogger<DynamicHlsHelper> logger,
+ IHttpContextAccessor httpContextAccessor)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dlnaManager = dlnaManager;
+ _authContext = authContext;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _subtitleEncoder = subtitleEncoder;
+ _configuration = configuration;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _networkManager = networkManager;
+ _logger = logger;
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ /// <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;
+ 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)
+ {
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ _httpContextAccessor.HttpContext.Request,
+ _authContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _fileSystem,
+ _subtitleEncoder,
+ _configuration,
+ _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 builder = new StringBuilder();
+
+ builder.AppendLine("#EXTM3U");
+
+ var isLiveStream = state.IsSegmentedLiveStream;
+
+ var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
+
+ // from universal audio service
+ if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
+ {
+ queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
+ }
+
+ // from universal audio service
+ if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
+ }
+
+ // Main stream
+ var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
+
+ playlistUrl += queryString;
+
+ var subtitleStreams = state.MediaSource
+ .MediaStreams
+ .Where(i => i.IsTextSubtitleStream)
+ .ToList();
+
+ 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 != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ subtitleGroup = null;
+ }
+
+ if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ {
+ AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User);
+ }
+
+ AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+
+ if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress))
+ {
+ var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
+
+ // By default, vary by just 200k
+ var variation = GetBitrateVariation(totalBitrate);
+
+ var newBitrate = totalBitrate - variation;
+ var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+
+ variation *= 2;
+ newBitrate = totalBitrate - variation;
+ variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+ }
+
+ return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
+
+ private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+ {
+ builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture))
+ .Append(",AVERAGE-BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture));
+
+ AppendPlaylistCodecsField(builder, state);
+
+ AppendPlaylistResolutionField(builder, state);
+
+ AppendPlaylistFramerateField(builder, state);
+
+ if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ {
+ builder.Append(",SUBTITLES=\"")
+ .Append(subtitleGroup)
+ .Append('"');
+ }
+
+ builder.Append(Environment.NewLine);
+ builder.AppendLine(url);
+ }
+
+ /// <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);
+ }
+
+ // Audio
+ string audioCodecs = string.Empty;
+ if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+ {
+ audioCodecs = GetPlaylistAudioCodecs(state);
+ }
+
+ StringBuilder codecs = new StringBuilder();
+
+ codecs.Append(videoCodecs);
+
+ if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
+ {
+ codecs.Append(',');
+ }
+
+ codecs.Append(audioCodecs);
+
+ if (codecs.Length > 1)
+ {
+ builder.Append(",CODECS=\"")
+ .Append(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)
+ {
+ if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
+ {
+ builder.Append(",RESOLUTION=")
+ .Append(state.OutputWidth.GetValueOrDefault())
+ .Append('x')
+ .Append(state.OutputHeight.GetValueOrDefault());
+ }
+ }
+
+ /// <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 != null)
+ {
+ framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
+ }
+
+ if (framerate.HasValue)
+ {
+ builder.Append(",FRAME-RATE=")
+ .Append(framerate.Value);
+ }
+ }
+
+ private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
+ {
+ // Within the local network this will likely do more harm than good.
+ var ip = RequestHelpers.NormalizeIp(ipAddress).ToString();
+ if (_networkManager.IsInLocalNetwork(ip))
+ {
+ return false;
+ }
+
+ if (!enableAdaptiveBitrateStreaming)
+ {
+ 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.OutputVideoCodec))
+ {
+ return false;
+ }
+
+ if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
+ {
+ return false;
+ }
+
+ if (!state.IsOutputVideo)
+ {
+ return false;
+ }
+
+ // Having problems in android
+ return false;
+ // return state.VideoRequest.VideoBitRate.HasValue;
+ }
+
+ private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
+ {
+ var selectedIndex = state.SubtitleStream == 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),
+ ClaimHelpers.GetToken(user));
+
+ 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)
+ {
+ string? levelString;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream.Level.HasValue)
+ {
+ levelString = state.VideoStream?.Level.ToString();
+ }
+ else
+ {
+ levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
+ }
+
+ if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
+ {
+ return parsedLevel;
+ }
+
+ return null;
+ }
+
+ /// <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, "mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetMP3String();
+ }
+
+ if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetAC3String();
+ }
+
+ if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetEAC3String();
+ }
+
+ 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;
+ }
+
+ if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+ return HlsCodecStringHelpers.GetH264String(profile, level);
+ }
+
+ if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
+
+ return HlsCodecStringHelpers.GetH265String(profile, level);
+ }
+
+ return string.Empty;
+ }
+
+ private int GetBitrateVariation(int bitrate)
+ {
+ // 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;
+ }
+
+ private string ReplaceBitrate(string url, int oldValue, int newValue)
+ {
+ return url.Replace(
+ "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
+ "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
+ StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
new file mode 100644
index 000000000..deb54dbe6
--- /dev/null
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -0,0 +1,137 @@
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.MediaEncoding;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// The stream response helpers.
+ /// </summary>
+ public static class FileStreamResponseHelpers
+ {
+ /// <summary>
+ /// Returns a static file from a remote source.
+ /// </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="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
+ /// <param name="httpContext">The current http context.</param>
+ /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
+ public static async Task<ActionResult> GetStaticRemoteStreamResult(
+ StreamState state,
+ bool isHeadRequest,
+ HttpClient httpClient,
+ HttpContext httpContext)
+ {
+ if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var 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(state.MediaPath).ConfigureAwait(false);
+ var contentType = response.Content.Headers.ContentType.ToString();
+
+ httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
+
+ if (isHeadRequest)
+ {
+ return new FileContentResult(Array.Empty<byte>(), contentType);
+ }
+
+ return new FileStreamResult(await response.Content.ReadAsStreamAsync().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>
+ /// <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>
+ /// <returns>An <see cref="ActionResult"/> the file.</returns>
+ public static ActionResult GetStaticFileResult(
+ string path,
+ string contentType,
+ bool isHeadRequest,
+ HttpContext httpContext)
+ {
+ httpContext.Response.ContentType = contentType;
+
+ // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
+ if (isHeadRequest)
+ {
+ return new NoContentResult();
+ }
+
+ return new PhysicalFileResult(path, contentType);
+ }
+
+ /// <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";
+
+ var contentType = state.GetMimeType(outputPath);
+
+ // Headers only
+ if (isHeadRequest)
+ {
+ return new FileContentResult(Array.Empty<byte>(), contentType);
+ }
+
+ var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ try
+ {
+ 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 memoryStream = new MemoryStream();
+ await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+ memoryStream.Position = 0;
+ return new FileStreamResult(memoryStream, contentType);
+ }
+ finally
+ {
+ transcodingLock.Release();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 3bbb77a65..95f1906ef 100644
--- a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -1,15 +1,14 @@
-using System;
+using System;
+using System.Globalization;
using System.Text;
-
-namespace MediaBrowser.Api.Playback
+namespace Jellyfin.Api.Helpers
{
/// <summary>
- /// Get various codec strings for use in HLS playlists.
+ /// Hls Codec string helpers.
/// </summary>
- static class HlsCodecStringFactory
+ public static class HlsCodecStringHelpers
{
-
/// <summary>
/// Gets a MP3 codec string.
/// </summary>
@@ -69,7 +68,7 @@ namespace MediaBrowser.Api.Playback
result.Append(".4240");
}
- string levelHex = level.ToString("X2");
+ string levelHex = level.ToString("X2", CultureInfo.InvariantCulture);
result.Append(levelHex);
return result.ToString();
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
new file mode 100644
index 000000000..242496697
--- /dev/null
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// The hls helpers.
+ /// </summary>
+ public static class HlsHelpers
+ {
+ /// <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);
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ 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.SequentialScan);
+ await using (fileStream.ConfigureAwait(false))
+ {
+ using var reader = new StreamReader(fileStream);
+ var count = 0;
+
+ while (!reader.EndOfStream)
+ {
+ var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+ if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ count++;
+ if (count >= segmentCount)
+ {
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Gets the hls playlist text.
+ /// </summary>
+ /// <param name="path">The path to the playlist file.</param>
+ /// <param name="segmentLength">The segment length.</param>
+ /// <returns>The playlist text as a string.</returns>
+ public static string GetLivePlaylistText(string path, int segmentLength)
+ {
+ using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var reader = new StreamReader(stream);
+
+ var text = reader.ReadToEnd();
+
+ text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.InvariantCulture);
+
+ var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
+
+ text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
+ // text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
+
+ return text;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index b7ca1a031..d2d1855a4 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -1,14 +1,10 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Buffers;
+using System;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -22,257 +18,85 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Services;
using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Api.Playback
+namespace Jellyfin.Api.Helpers
{
- [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
- public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- [Route("/Items/{Id}/PlaybackInfo", "POST", Summary = "Gets live playback media info for an item")]
- public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
- {
- }
-
- [Route("/LiveStreams/Open", "POST", Summary = "Opens a media source")]
- public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
+ /// <summary>
+ /// Media info helper.
+ /// </summary>
+ public class MediaInfoHelper
{
- }
-
- [Route("/LiveStreams/Close", "POST", Summary = "Closes a media source")]
- public class CloseMediaSource : IReturnVoid
- {
- [ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string LiveStreamId { get; set; }
- }
-
- [Route("/Playback/BitrateTest", "GET")]
- public class GetBitrateTestBytes
- {
- [ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int Size { get; set; }
-
- public GetBitrateTestBytes()
- {
- // 100k
- Size = 102400;
- }
- }
-
- [Authenticated]
- public class MediaInfoService : BaseApiService
- {
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IDeviceManager _deviceManager;
+ private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
- private readonly INetworkManager _networkManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IUserManager _userManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ILogger<MediaInfoHelper> _logger;
+ private readonly INetworkManager _networkManager;
+ private readonly IDeviceManager _deviceManager;
private readonly IAuthorizationContext _authContext;
- public MediaInfoService(
- ILogger<MediaInfoService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IMediaSourceManager mediaSourceManager,
- 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>
+ /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ public MediaInfoHelper(
+ IUserManager userManager,
ILibraryManager libraryManager,
- INetworkManager networkManager,
+ IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
- IUserManager userManager,
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<MediaInfoHelper> logger,
+ INetworkManager networkManager,
+ IDeviceManager deviceManager,
IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
{
- _mediaSourceManager = mediaSourceManager;
- _deviceManager = deviceManager;
+ _userManager = userManager;
_libraryManager = libraryManager;
- _networkManager = networkManager;
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
- _userManager = userManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _logger = logger;
+ _networkManager = networkManager;
+ _deviceManager = deviceManager;
_authContext = authContext;
}
- public object Get(GetBitrateTestBytes request)
- {
- const int MaxSize = 10_000_000;
-
- var size = request.Size;
-
- if (size <= 0)
- {
- throw new ArgumentException($"The requested size ({size}) is equal to or smaller than 0.", nameof(request));
- }
-
- if (size > MaxSize)
- {
- throw new ArgumentException($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).", nameof(request));
- }
-
- byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
- try
- {
- new Random().NextBytes(buffer);
- return ResultFactory.GetResult(null, buffer, "application/octet-stream");
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(buffer);
- }
- }
-
- public async Task<object> Get(GetPlaybackInfo request)
- {
- var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(OpenMediaSource request)
- {
- var result = await OpenMediaSource(request).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- private async Task<LiveStreamResponse> OpenMediaSource(OpenMediaSource request)
- {
- var authInfo = _authContext.GetAuthorizationInfo(Request);
-
- var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
-
- var profile = request.DeviceProfile;
- if (profile == null)
- {
- var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
- if (caps != null)
- {
- profile = caps.DeviceProfile;
- }
- }
-
- if (profile != null)
- {
- var item = _libraryManager.GetItemById(request.ItemId);
-
- SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
- request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
- request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, true, true, true);
- }
- else
- {
- if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
- {
- result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
- }
- }
-
- if (result.MediaSource != null)
- {
- NormalizeMediaSourceContainer(result.MediaSource, profile, DlnaProfileType.Video);
- }
-
- return result;
- }
-
- public void Post(CloseMediaSource request)
- {
- _mediaSourceManager.CloseLiveStream(request.LiveStreamId).GetAwaiter().GetResult();
- }
-
- public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
- {
- var authInfo = _authContext.GetAuthorizationInfo(Request);
-
- var profile = request.DeviceProfile;
-
- Logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
-
- if (profile == null)
- {
- var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
- if (caps != null)
- {
- profile = caps.DeviceProfile;
- }
- }
-
- var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
-
- if (profile != null)
- {
- var mediaSourceId = request.MediaSourceId;
-
- SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, request.EnableTranscoding, request.AllowVideoStreamCopy, request.AllowAudioStreamCopy);
- }
-
- if (request.AutoOpenLiveStream)
- {
- var mediaSource = string.IsNullOrWhiteSpace(request.MediaSourceId) ? info.MediaSources.FirstOrDefault() : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId, StringComparison.Ordinal));
-
- if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
- {
- var openStreamResult = await OpenMediaSource(new OpenMediaSource
- {
- AudioStreamIndex = request.AudioStreamIndex,
- DeviceProfile = request.DeviceProfile,
- EnableDirectPlay = request.EnableDirectPlay,
- EnableDirectStream = request.EnableDirectStream,
- ItemId = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MaxStreamingBitrate = request.MaxStreamingBitrate,
- PlaySessionId = info.PlaySessionId,
- StartTimeTicks = request.StartTimeTicks,
- SubtitleStreamIndex = request.SubtitleStreamIndex,
- UserId = request.UserId,
- OpenToken = mediaSource.OpenToken
- }).ConfigureAwait(false);
-
- info.MediaSources = new[] { openStreamResult.MediaSource };
- }
- }
-
- if (info.MediaSources != null)
- {
- foreach (var mediaSource in info.MediaSources)
- {
- NormalizeMediaSourceContainer(mediaSource, profile, DlnaProfileType.Video);
- }
- }
-
- return info;
- }
-
- private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
- {
- mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
- }
-
- public async Task<object> Post(GetPostedPlaybackInfo request)
+ /// <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 result = await GetPlaybackInfo(request).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
- {
- var user = _userManager.GetUserById(userId);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
MediaSourceInfo[] mediaSources;
if (string.IsNullOrWhiteSpace(liveStreamId))
{
-
- // TODO handle supportedLiveMediaTypes?
+ // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(mediaSourceId))
@@ -297,10 +121,7 @@ namespace MediaBrowser.Api.Playback
{
result.MediaSources = Array.Empty<MediaSourceInfo>();
- if (!result.ErrorCode.HasValue)
- {
- result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
- }
+ result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
}
else
{
@@ -314,36 +135,28 @@ namespace MediaBrowser.Api.Playback
return result;
}
- private void SetDeviceSpecificData(
- Guid itemId,
- PlaybackInfoResponse result,
- DeviceProfile profile,
- AuthorizationInfo auth,
- long? maxBitrate,
- long startTimeTicks,
- string mediaSourceId,
- int? audioStreamIndex,
- int? subtitleStreamIndex,
- int? maxAudioChannels,
- Guid userId,
- bool enableDirectPlay,
- bool forceDirectPlayRemoteMediaSource,
- bool enableDirectStream,
- bool enableTranscoding,
- bool allowVideoStreamCopy,
- bool allowAudioStreamCopy)
- {
- var item = _libraryManager.GetItemById(itemId);
-
- foreach (var mediaSource in result.MediaSources)
- {
- SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding, allowVideoStreamCopy, allowAudioStreamCopy);
- }
-
- SortMediaSources(result, maxBitrate);
- }
-
- private void SetDeviceSpecificData(
+ /// <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="auth">Authorization info.</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,
@@ -357,13 +170,13 @@ namespace MediaBrowser.Api.Playback
string playSessionId,
Guid userId,
bool enableDirectPlay,
- bool forceDirectPlayRemoteMediaSource,
bool enableDirectStream,
bool enableTranscoding,
bool allowVideoStreamCopy,
- bool allowAudioStreamCopy)
+ bool allowAudioStreamCopy,
+ string ipAddress)
{
- var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
+ var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
var options = new VideoOptions
{
@@ -401,14 +214,15 @@ namespace MediaBrowser.Api.Playback
if (item is Audio)
{
- Logger.LogInformation(
+ _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}",
+ _logger.LogInformation(
+ "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
user.Username,
user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
@@ -475,7 +289,7 @@ namespace MediaBrowser.Api.Playback
}
else
{
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
+ options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
if (item is Audio)
{
@@ -513,7 +327,7 @@ namespace MediaBrowser.Api.Playback
if (mediaSource.SupportsTranscoding)
{
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
+ options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
@@ -586,28 +400,125 @@ namespace MediaBrowser.Api.Playback
}
}
- private long? GetMaxBitrate(long? clientMaxBitrate, Jellyfin.Data.Entities.User user)
+ /// <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 maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
+ var originalList = result.MediaSources.ToList();
- if (remoteClientMaxBitrate <= 0)
+ result.MediaSources = result.MediaSources.OrderBy(i =>
+ {
+ // Nothing beats direct playing a file
+ if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
+ {
+ return 0;
+ }
+
+ 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)
+ {
+ return 0;
+ }
+
+ return 1;
+ })
+ .ThenBy(i =>
+ {
+ return i.Protocol switch
+ {
+ MediaProtocol.File => 0,
+ _ => 1,
+ };
+ })
+ .ThenBy(i =>
+ {
+ if (maxBitrate.HasValue && i.Bitrate.HasValue)
+ {
+ return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
+ }
+
+ return 1;
+ })
+ .ThenBy(originalList.IndexOf)
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Open media source.
+ /// </summary>
+ /// <param name="httpRequest">Http Request.</param>
+ /// <param name="request">Live stream request.</param>
+ /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
+ public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request)
+ {
+ var authInfo = _authContext.GetAuthorizationInfo(httpRequest);
+
+ var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
+
+ var profile = request.DeviceProfile;
+ if (profile == null)
{
- remoteClientMaxBitrate = ServerConfigurationManager.Configuration.RemoteClientBitrateLimit;
+ var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
+ if (clientCapabilities != null)
+ {
+ profile = clientCapabilities.DeviceProfile;
+ }
}
- if (remoteClientMaxBitrate > 0)
+ if (profile != null)
{
- var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.RemoteIp);
+ var item = _libraryManager.GetItemById(request.ItemId);
- Logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.RemoteIp, isInLocalNetwork);
- if (!isInLocalNetwork)
+ SetDeviceSpecificData(
+ item,
+ result.MediaSource,
+ profile,
+ authInfo,
+ 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,
+ httpRequest.HttpContext.Connection.RemoteIpAddress.ToString());
+ }
+ else
+ {
+ if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
{
- maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
+ result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
}
}
- return maxBitrate;
+ // here was a check if (result.MediaSource != null) but Rider said it will never be null
+ NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
+
+ return result;
+ }
+
+ /// <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, mediaSource.Path, profile, type);
}
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
@@ -635,45 +546,28 @@ namespace MediaBrowser.Api.Playback
}
}
- private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
+ private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
{
- var originalList = result.MediaSources.ToList();
+ var maxBitrate = clientMaxBitrate;
+ var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
- result.MediaSources = result.MediaSources.OrderBy(i =>
+ if (remoteClientMaxBitrate <= 0)
{
- // Nothing beats direct playing a file
- if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
- {
- return 0;
- }
+ remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
+ }
- return 1;
- }).ThenBy(i =>
+ if (remoteClientMaxBitrate > 0)
{
- // Let's assume direct streaming a file is just as desirable as direct playing a remote url
- if (i.SupportsDirectPlay || i.SupportsDirectStream)
- {
- return 0;
- }
+ var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
- return 1;
- }).ThenBy(i =>
- {
- return i.Protocol switch
- {
- MediaProtocol.File => 0,
- _ => 1,
- };
- }).ThenBy(i =>
- {
- if (maxBitrate.HasValue && i.Bitrate.HasValue)
+ _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
+ if (!isInLocalNetwork)
{
- return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
+ maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
}
+ }
- return 1;
- }).ThenBy(originalList.IndexOf)
- .ToArray();
+ return maxBitrate;
}
}
}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
new file mode 100644
index 000000000..432df9708
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Buffers;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// Progressive file copier.
+ /// </summary>
+ public class ProgressiveFileCopier
+ {
+ private readonly TranscodingJobDto? _job;
+ private readonly string? _path;
+ private readonly CancellationToken _cancellationToken;
+ private readonly IDirectStreamProvider? _directStreamProvider;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private long _bytesWritten;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
+ /// </summary>
+ /// <param name="path">The path to copy from.</param>
+ /// <param name="job">The transcoding job.</param>
+ /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
+ {
+ _path = path;
+ _job = job;
+ _cancellationToken = cancellationToken;
+ _transcodingJobHelper = transcodingJobHelper;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
+ /// </summary>
+ /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
+ /// <param name="job">The transcoding job.</param>
+ /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
+ {
+ _directStreamProvider = directStreamProvider;
+ _job = job;
+ _cancellationToken = cancellationToken;
+ _transcodingJobHelper = transcodingJobHelper;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether allow read end of file.
+ /// </summary>
+ public bool AllowEndOfFile { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets copy start position.
+ /// </summary>
+ public long StartPosition { get; set; }
+
+ /// <summary>
+ /// Write source stream to output.
+ /// </summary>
+ /// <param name="outputStream">Output stream.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>A <see cref="Task"/>.</returns>
+ public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
+ {
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
+
+ try
+ {
+ if (_directStreamProvider != null)
+ {
+ await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var fileOptions = FileOptions.SequentialScan;
+ var allowAsyncFileRead = false;
+
+ // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ fileOptions |= FileOptions.Asynchronous;
+ allowAsyncFileRead = true;
+ }
+
+ await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+
+ var eofCount = 0;
+ const int EmptyReadLimit = 20;
+ if (StartPosition > 0)
+ {
+ inputStream.Position = StartPosition;
+ }
+
+ while (eofCount < EmptyReadLimit || !AllowEndOfFile)
+ {
+ var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
+
+ if (bytesRead == 0)
+ {
+ if (_job == null || _job.HasExited)
+ {
+ eofCount++;
+ }
+
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ eofCount = 0;
+ }
+ }
+ }
+ finally
+ {
+ if (_job != null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(_job);
+ }
+ }
+ }
+
+ private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
+ {
+ var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
+ int bytesRead;
+ int totalBytesRead = 0;
+
+ if (readAsync)
+ {
+ bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ bytesRead = source.Read(array, 0, array.Length);
+ }
+
+ while (bytesRead != 0)
+ {
+ var bytesToWrite = bytesRead;
+
+ if (bytesToWrite > 0)
+ {
+ await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+
+ _bytesWritten += bytesRead;
+ totalBytesRead += bytesRead;
+
+ if (_job != null)
+ {
+ _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+ }
+ }
+
+ if (readAsync)
+ {
+ bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ bytesRead = source.Read(array, 0, array.Length);
+ }
+ }
+
+ return totalBytesRead;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
new file mode 100644
index 000000000..fbaa69270
--- /dev/null
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// Request Extensions.
+ /// </summary>
+ public static class RequestHelpers
+ {
+ /// <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 ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
+ {
+ var val = sortBy;
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return Array.Empty<ValueTuple<string, SortOrder>>();
+ }
+
+ var vals = val.Split(',');
+ if (string.IsNullOrWhiteSpace(requestedSortOrder))
+ {
+ requestedSortOrder = "Ascending";
+ }
+
+ var sortOrders = requestedSortOrder.Split(',');
+
+ var result = new ValueTuple<string, SortOrder>[vals.Length];
+
+ for (var i = 0; i < vals.Length; i++)
+ {
+ var sortOrderIndex = sortOrders.Length > i ? i : 0;
+
+ var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
+ var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
+ ? SortOrder.Descending
+ : SortOrder.Ascending;
+
+ result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Get parsed filters.
+ /// </summary>
+ /// <param name="filters">The filters.</param>
+ /// <returns>Item filters.</returns>
+ public static IEnumerable<ItemFilter> GetFilters(string? filters)
+ {
+ return string.IsNullOrEmpty(filters)
+ ? Array.Empty<ItemFilter>()
+ : filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
+ }
+
+ /// <summary>
+ /// Splits a string at a separating character into an array of substrings.
+ /// </summary>
+ /// <param name="value">The string to split.</param>
+ /// <param name="separator">The char that separates the substrings.</param>
+ /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
+ /// <returns>An array of the substrings.</returns>
+ internal static string[] Split(string? value, char separator, bool removeEmpty)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return Array.Empty<string>();
+ }
+
+ return removeEmpty
+ ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
+ : value.Split(separator);
+ }
+
+ /// <summary>
+ /// Checks if the user can update an entry.
+ /// </summary>
+ /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ /// <param name="requestContext">The <see cref="HttpRequest"/>.</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(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
+ {
+ var auth = authContext.GetAuthorizationInfo(requestContext);
+
+ var authenticatedUser = auth.User;
+
+ // If they're going to update the record of another user, they must be an administrator
+ if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
+ || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
+ {
+ var authorization = authContext.GetAuthorizationInfo(request);
+ var user = authorization.User;
+ var session = sessionManager.LogSessionActivity(
+ authorization.Client,
+ authorization.Version,
+ authorization.DeviceId,
+ authorization.Device,
+ request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ user);
+
+ if (session == null)
+ {
+ throw new ArgumentException("Session not found.");
+ }
+
+ return session;
+ }
+
+ /// <summary>
+ /// Get Guid array from string.
+ /// </summary>
+ /// <param name="value">String value.</param>
+ /// <returns>Guid array.</returns>
+ internal static Guid[] GetGuids(string? value)
+ {
+ if (value == null)
+ {
+ return Array.Empty<Guid>();
+ }
+
+ return Split(value, ',', true)
+ .Select(i => new Guid(i))
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Gets the item fields.
+ /// </summary>
+ /// <param name="fields">The fields string.</param>
+ /// <returns>IEnumerable{ItemFields}.</returns>
+ internal static ItemFields[] GetItemFields(string? fields)
+ {
+ if (string.IsNullOrEmpty(fields))
+ {
+ return Array.Empty<ItemFields>();
+ }
+
+ return Split(fields, ',', true)
+ .Select(v =>
+ {
+ if (Enum.TryParse(v, true, out ItemFields value))
+ {
+ return (ItemFields?)value;
+ }
+
+ return null;
+ }).Where(i => i.HasValue)
+ .Select(i => i!.Value)
+ .ToArray();
+ }
+
+ internal static IPAddress NormalizeIp(IPAddress ip)
+ {
+ return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
index 84abf7b8d..b922e76cf 100644
--- a/MediaBrowser.Api/SimilarItemsHelper.cs
+++ b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
@@ -1,95 +1,48 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Api
+namespace Jellyfin.Api.Helpers
{
/// <summary>
- /// Class BaseGetSimilarItemsFromItem.
- /// </summary>
- public class BaseGetSimilarItemsFromItem : BaseGetSimilarItems
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- public string ExcludeArtistIds { get; set; }
- }
-
- public class BaseGetSimilarItems : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
- }
-
- /// <summary>
- /// Class SimilarItemsHelper.
+ /// The similar items helper class.
/// </summary>
public static class SimilarItemsHelper
{
- internal static QueryResult<BaseItemDto> GetSimilarItemsResult(DtoOptions dtoOptions, IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, BaseGetSimilarItemsFromItem request, Type[] includeTypes, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
+ internal static QueryResult<BaseItemDto> GetSimilarItemsResult(
+ DtoOptions dtoOptions,
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ Guid? userId,
+ string id,
+ string? excludeArtistIds,
+ int? limit,
+ Type[] includeTypes,
+ Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
{
- var user = !request.UserId.Equals(Guid.Empty) ? userManager.GetUserById(request.UserId) : null;
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? userManager.GetUserById(userId.Value)
+ : null;
- var item = string.IsNullOrEmpty(request.Id) ?
- (!request.UserId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
- libraryManager.RootFolder) : libraryManager.GetItemById(request.Id);
+ var item = string.IsNullOrEmpty(id) ?
+ (!userId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
+ libraryManager.RootFolder) : libraryManager.GetItemById(id);
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(),
Recursive = true,
- DtoOptions = dtoOptions
+ DtoOptions = dtoOptions,
+ ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds)
};
- // ExcludeArtistIds
- if (!string.IsNullOrEmpty(request.ExcludeArtistIds))
- {
- query.ExcludeArtistIds = BaseApiService.GetGuids(request.ExcludeArtistIds);
- }
-
var inputItems = libraryManager.GetItemList(query);
var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore)
@@ -97,9 +50,9 @@ namespace MediaBrowser.Api
var returnItems = items;
- if (request.Limit.HasValue)
+ if (limit.HasValue)
{
- returnItems = returnItems.Take(request.Limit.Value).ToList();
+ returnItems = returnItems.Take(limit.Value).ToList();
}
var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
@@ -107,7 +60,6 @@ namespace MediaBrowser.Api
return new QueryResult<BaseItemDto>
{
Items = dtos,
-
TotalRecordCount = items.Count
};
}
@@ -120,7 +72,11 @@ namespace MediaBrowser.Api
/// <param name="inputItems">The input items.</param>
/// <param name="getSimilarityScore">The get similarity score.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
- internal static IEnumerable<BaseItem> GetSimilaritems(BaseItem item, ILibraryManager libraryManager, IEnumerable<BaseItem> inputItems, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
+ private static IEnumerable<BaseItem> GetSimilaritems(
+ BaseItem item,
+ ILibraryManager libraryManager,
+ IEnumerable<BaseItem> inputItems,
+ Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
{
var itemId = item.Id;
inputItems = inputItems.Where(i => i.Id != itemId);
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
new file mode 100644
index 000000000..b12590080
--- /dev/null
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -0,0 +1,758 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// The streaming helpers.
+ /// </summary>
+ public static class StreamingHelpers
+ {
+ /// <summary>
+ /// Gets the current streaming state.
+ /// </summary>
+ /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
+ /// <param name="httpRequest">The <see cref="HttpRequest"/>.</param>
+ /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</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="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <param name="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,
+ HttpRequest httpRequest,
+ IAuthorizationContext authorizationContext,
+ IMediaSourceManager mediaSourceManager,
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ ISubtitleEncoder subtitleEncoder,
+ IConfiguration configuration,
+ IDlnaManager dlnaManager,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ TranscodingJobType transcodingJobType,
+ CancellationToken cancellationToken)
+ {
+ EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+ // Parse the DLNA time seek header
+ if (!streamingRequest.StartTimeTicks.HasValue)
+ {
+ var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
+
+ streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
+ }
+
+ if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
+ {
+ ParseParams(streamingRequest);
+ }
+
+ streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
+
+ var url = httpRequest.Path.Value.Split('.').Last();
+
+ if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
+ {
+ streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
+ }
+
+ var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
+ 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 auth = authorizationContext.GetAuthorizationInfo(httpRequest);
+ if (!auth.UserId.Equals(Guid.Empty))
+ {
+ state.User = userManager.GetUserById(auth.UserId);
+ }
+
+ 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(i => mediaEncoder.CanEncodeToAudioCodec(i))
+ ?? state.SupportedAudioCodecs.FirstOrDefault();
+ }
+
+ if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
+ {
+ state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
+ ?? state.SupportedSubtitleCodecs.FirstOrDefault();
+ }
+
+ var item = libraryManager.GetItemById(streamingRequest.Id);
+
+ 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;
+
+ if (currentJob != null)
+ {
+ mediaSource = currentJob.MediaSource;
+ }
+
+ if (mediaSource == 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.InvariantCulture));
+
+ if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id)
+ {
+ mediaSource = mediaSources[0];
+ }
+ }
+ }
+ else
+ {
+ var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
+ mediaSource = liveStreamInfo.Item1;
+ state.DirectStreamProvider = liveStreamInfo.Item2;
+ }
+
+ encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
+
+ string? containerInternal = Path.GetExtension(state.RequestedUrl);
+
+ if (string.IsNullOrEmpty(streamingRequest.Container))
+ {
+ containerInternal = streamingRequest.Container;
+ }
+
+ if (string.IsNullOrEmpty(containerInternal))
+ {
+ containerInternal = streamingRequest.Static ?
+ StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio)
+ : GetOutputFileExtension(state);
+ }
+
+ state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
+
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
+
+ state.OutputAudioCodec = streamingRequest.AudioCodec;
+
+ state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
+
+ if (state.VideoRequest != null)
+ {
+ state.OutputVideoCodec = state.Request.VideoCodec;
+ state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
+
+ encodingHelper.TryStreamCopy(state);
+
+ if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ var resolution = ResolutionNormalizer.Normalize(
+ state.VideoStream?.BitRate,
+ state.VideoStream?.Width,
+ state.VideoStream?.Height,
+ state.OutputVideoBitrate.Value,
+ state.VideoStream?.Codec,
+ state.OutputVideoCodec,
+ 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);
+
+ var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
+ ? GetOutputFileExtension(state)
+ : ('.' + state.OutputContainer);
+
+ state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+
+ 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)
+ {
+ if (!state.EnableDlnaHeaders)
+ {
+ return;
+ }
+
+ 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=*");
+
+ if (state.RunTimeTicks.HasValue)
+ {
+ 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)));
+ }
+
+ if (!isStaticallyStreamed && profile != null)
+ {
+ AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
+ }
+ }
+
+ if (profile == null)
+ {
+ profile = dlnaManager.GetDefaultProfile();
+ }
+
+ var audioCodec = state.ActualOutputAudioCodec;
+
+ if (!state.IsVideoRequest)
+ {
+ responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader(
+ 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",
+ new ContentFeatureBuilder(profile).BuildVideoHeader(state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, 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)
+ {
+ if (value.IsEmpty)
+ {
+ return null;
+ }
+
+ 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)
+ {
+ // 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");
+ }
+
+ try
+ {
+ // Parses npt times in the format of '10:19:25.7'
+ return TimeSpan.Parse(value).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)
+ {
+ 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;
+ }
+ }
+
+ 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);
+
+ 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>
+ /// <returns>System.String.</returns>
+ private static string? GetOutputFileExtension(StreamState state)
+ {
+ var ext = Path.GetExtension(state.RequestedUrl);
+
+ if (!string.IsNullOrEmpty(ext))
+ {
+ return ext;
+ }
+
+ // Try to infer based on the desired video codec
+ if (state.IsVideoRequest)
+ {
+ var videoCodec = state.Request.VideoCodec;
+
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".ts";
+ }
+
+ if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".ogv";
+ }
+
+ if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".webm";
+ }
+
+ if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".asf";
+ }
+ }
+
+ // Try to infer based on the desired audio codec
+ if (!state.IsVideoRequest)
+ {
+ var audioCodec = state.Request.AudioCodec;
+
+ if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".aac";
+ }
+
+ if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".mp3";
+ }
+
+ if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".ogg";
+ }
+
+ if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".wma";
+ }
+ }
+
+ return null;
+ }
+
+ /// <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!}";
+
+ var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ var ext = outputFileExtension?.ToLowerInvariant();
+ var folder = serverConfigurationManager.GetTranscodePath();
+
+ return Path.Combine(folder, filename + ext);
+ }
+
+ private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
+ {
+ var headers = request.Headers;
+
+ if (!string.IsNullOrWhiteSpace(deviceProfileId))
+ {
+ state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
+ }
+ else if (!string.IsNullOrWhiteSpace(deviceProfileId))
+ {
+ var caps = deviceManager.GetCapabilities(deviceProfileId);
+
+ state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
+ }
+
+ var profile = state.DeviceProfile;
+
+ if (profile == null)
+ {
+ // Don't use settings from the default profile.
+ // Only use a specific profile if it was requested.
+ return;
+ }
+
+ 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.TargetVideoLevel,
+ state.TargetFramerate,
+ state.TargetPacketLength,
+ state.TargetTimestamp,
+ state.IsTargetAnamorphic,
+ state.IsTargetInterlaced,
+ state.TargetRefFrames,
+ state.TargetVideoStreamCount,
+ state.TargetAudioStreamCount,
+ state.TargetVideoCodecTag,
+ state.IsTargetAVC);
+
+ if (mediaProfile != 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 != null)
+ {
+ state.EstimateContentLength = transcodingProfile.EstimateContentLength;
+ // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
+ state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+
+ if (state.VideoRequest != null)
+ {
+ 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)
+ {
+ if (string.IsNullOrEmpty(request.Params))
+ {
+ return;
+ }
+
+ var vals = request.Params.Split(';');
+
+ var videoRequest = request as VideoRequestDto;
+
+ for (var i = 0; i < vals.Length; i++)
+ {
+ var val = vals[i];
+
+ 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 != null)
+ {
+ videoRequest.VideoCodec = val;
+ }
+
+ break;
+ case 5:
+ request.AudioCodec = val;
+ break;
+ case 6:
+ if (videoRequest != null)
+ {
+ videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+ }
+
+ break;
+ case 7:
+ if (videoRequest != null)
+ {
+ videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+ }
+
+ break;
+ case 8:
+ if (videoRequest != 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 != null)
+ {
+ videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
+ }
+
+ break;
+ case 12:
+ if (videoRequest != null)
+ {
+ videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
+ }
+
+ break;
+ case 13:
+ if (videoRequest != null)
+ {
+ videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
+ }
+
+ break;
+ case 14:
+ request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 15:
+ if (videoRequest != null)
+ {
+ videoRequest.Level = val;
+ }
+
+ break;
+ case 16:
+ if (videoRequest != null)
+ {
+ videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
+ }
+
+ break;
+ case 17:
+ if (videoRequest != null)
+ {
+ videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
+ }
+
+ break;
+ case 18:
+ if (videoRequest != 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 != null)
+ {
+ videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
+
+ break;
+ case 25:
+ if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
+ {
+ if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
+ {
+ videoRequest.SubtitleMethod = method;
+ }
+ }
+
+ break;
+ case 26:
+ request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 27:
+ if (videoRequest != null)
+ {
+ videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
+
+ break;
+ case 28:
+ request.Tag = val;
+ break;
+ case 29:
+ if (videoRequest != null)
+ {
+ videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
+
+ break;
+ case 30:
+ request.SubtitleCodec = val;
+ break;
+ case 31:
+ if (videoRequest != null)
+ {
+ videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
+
+ break;
+ case 32:
+ if (videoRequest != null)
+ {
+ videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
+
+ break;
+ case 33:
+ request.TranscodeReasons = val;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
new file mode 100644
index 000000000..67e450372
--- /dev/null
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -0,0 +1,886 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// Transcoding job helpers.
+ /// </summary>
+ public class TranscodingJobHelper : IDisposable
+ {
+ /// <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 IAuthorizationContext _authorizationContext;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly IFileSystem _fileSystem;
+ private readonly IIsoManager _isoManager;
+ 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;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
+ /// </summary>
+ /// <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="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ /// <param name="isoManager">Instance of the <see cref="IIsoManager"/> interface.</param>
+ /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public TranscodingJobHelper(
+ ILogger<TranscodingJobHelper> logger,
+ IMediaSourceManager mediaSourceManager,
+ IFileSystem fileSystem,
+ IMediaEncoder mediaEncoder,
+ IServerConfigurationManager serverConfigurationManager,
+ ISessionManager sessionManager,
+ IAuthorizationContext authorizationContext,
+ IIsoManager isoManager,
+ ISubtitleEncoder subtitleEncoder,
+ IConfiguration configuration,
+ ILoggerFactory loggerFactory)
+ {
+ _logger = logger;
+ _mediaSourceManager = mediaSourceManager;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _serverConfigurationManager = serverConfigurationManager;
+ _sessionManager = sessionManager;
+ _authorizationContext = authorizationContext;
+ _isoManager = isoManager;
+ _loggerFactory = loggerFactory;
+ _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+
+ 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));
+ }
+ }
+
+ /// <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)
+ {
+ return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, 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)
+ {
+ if (string.IsNullOrEmpty(playSessionId))
+ {
+ throw new ArgumentNullException(nameof(playSessionId));
+ }
+
+ _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
+
+ 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)
+ {
+ _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
+ job.IsUserPaused = isUserPaused.Value;
+ }
+
+ PingTimer(job, true);
+ }
+ }
+
+ private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn)
+ {
+ if (job.HasExited)
+ {
+ job.StopKillTimer();
+ return;
+ }
+
+ var timerDuration = 10000;
+
+ if (job.Type != TranscodingJobType.Progressive)
+ {
+ timerDuration = 60000;
+ }
+
+ job.PingTimeout = timerDuration;
+ job.LastPingDate = DateTime.UtcNow;
+
+ // Don't start the timer for playback checkins with progressive streaming
+ if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
+ {
+ job.StartKillTimer(OnTranscodeKillTimerStopped);
+ }
+ else
+ {
+ job.ChangeKillTimerIfStarted();
+ }
+ }
+
+ /// <summary>
+ /// Called when [transcode kill timer stopped].
+ /// </summary>
+ /// <param name="state">The state.</param>
+ private async void OnTranscodeKillTimerStopped(object state)
+ {
+ var job = (TranscodingJobDto)state;
+
+ if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
+ {
+ var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
+
+ if (timeSinceLastPing < job.PingTimeout)
+ {
+ job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
+ return;
+ }
+ }
+
+ _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+
+ await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
+ }
+
+ /// <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);
+ }
+
+ /// <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>();
+
+ lock (_activeTranscodingJobs)
+ {
+ // This is really only needed for HLS.
+ // Progressive streams can stop on their own reliably
+ jobs.AddRange(_activeTranscodingJobs.Where(killJob));
+ }
+
+ if (jobs.Count == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ IEnumerable<Task> GetKillJobs()
+ {
+ foreach (var job in jobs)
+ {
+ yield return KillTranscodingJob(job, false, deleteFiles);
+ }
+ }
+
+ return Task.WhenAll(GetKillJobs());
+ }
+
+ /// <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();
+
+ _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+
+ lock (_activeTranscodingJobs)
+ {
+ _activeTranscodingJobs.Remove(job);
+
+ if (!job.CancellationTokenSource!.IsCancellationRequested)
+ {
+ job.CancellationTokenSource.Cancel();
+ }
+ }
+
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(job.Path!);
+ }
+
+ lock (job.ProcessLock!)
+ {
+ job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
+
+ var process = job.Process;
+
+ var hasExited = job.HasExited;
+
+ if (!hasExited)
+ {
+ try
+ {
+ _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
+
+ 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)
+ {
+ }
+ }
+ }
+
+ if (delete(job.Path!))
+ {
+ await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+ }
+
+ if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
+ {
+ try
+ {
+ await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
+ }
+ }
+ }
+
+ private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
+ {
+ if (retryCount >= 10)
+ {
+ return;
+ }
+
+ _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
+
+ await Task.Delay(delayMs).ConfigureAwait(false);
+
+ 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 DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
+ }
+ catch (Exception 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)
+ {
+ if (File.Exists(outputFilePath))
+ {
+ _fileSystem.DeleteFile(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);
+ var name = Path.GetFileNameWithoutExtension(outputFilePath);
+
+ var filesToDelete = _fileSystem.GetFilePaths(directory)
+ .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
+
+ List<Exception>? exs = null;
+ foreach (var file in filesToDelete)
+ {
+ 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);
+ }
+ }
+
+ if (exs != null)
+ {
+ throw new AggregateException("Error deleting HLS files", exs);
+ }
+ }
+
+ /// <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 (job != null)
+ {
+ job.Framerate = framerate;
+ job.CompletionPercentage = percentComplete;
+ job.TranscodingPositionTicks = ticks;
+ job.BytesTranscoded = bytesTranscoded;
+ job.BitRate = bitRate;
+ }
+
+ var deviceId = state.Request.DeviceId;
+
+ if (!string.IsNullOrWhiteSpace(deviceId))
+ {
+ var audioCodec = state.ActualOutputAudioCodec;
+ var videoCodec = state.ActualOutputVideoCodec;
+
+ _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),
+ TranscodeReasons = state.TranscodeReasons
+ });
+ }
+ }
+
+ /// <summary>
+ /// Starts the 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)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+
+ await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
+
+ if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ var auth = _authorizationContext.GetAuthorizationInfo(request);
+ if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
+ {
+ this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+
+ throw new ArgumentException("User does not have access to video transcoding");
+ }
+ }
+
+ 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) ? null : 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);
+
+ var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+ _logger.LogInformation(commandLineLogMessage);
+
+ var logFilePrefix = "ffmpeg-transcode";
+ if (state.VideoRequest != null
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
+ ? "ffmpeg-remux"
+ : "ffmpeg-directstream";
+ }
+
+ var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
+
+ // 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, true);
+
+ 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, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
+
+ process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
+
+ try
+ {
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting ffmpeg");
+
+ this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+
+ throw;
+ }
+
+ _logger.LogDebug("Launched ffmpeg process");
+ state.TranscodingJob = transcodingJob;
+
+ // 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);
+
+ // Wait for the file to exist before proceeeding
+ 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);
+ }
+
+ _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
+
+ if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
+ {
+ await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
+
+ if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
+ {
+ await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ }
+
+ if (!transcodingJob.HasExited)
+ {
+ StartThrottler(state, transcodingJob);
+ }
+
+ _logger.LogDebug("StartFfMpeg() finished successfully");
+
+ return transcodingJob;
+ }
+
+ private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
+ {
+ if (EnableThrottling(state))
+ {
+ transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
+ state.TranscodingThrottler.Start();
+ }
+ }
+
+ private bool EnableThrottling(StreamState state)
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+
+ // enable throttling when NOT using hardware acceleration
+ if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+ {
+ return state.InputProtocol == MediaProtocol.File &&
+ state.RunTimeTicks.HasValue &&
+ state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
+ state.IsInputVideo &&
+ state.VideoType == VideoType.VideoFile &&
+ !EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
+ }
+
+ return false;
+ }
+
+ /// <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;
+ }
+ }
+
+ /// <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)
+ {
+ 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)
+ {
+ lock (_activeTranscodingJobs)
+ {
+ var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+
+ if (job != null)
+ {
+ _activeTranscodingJobs.Remove(job);
+ }
+ }
+
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(path);
+ }
+
+ if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
+ {
+ _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
+ }
+ }
+
+ /// <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 (job != null)
+ {
+ job.HasExited = true;
+ }
+
+ _logger.LogDebug("Disposing stream resources");
+ state.Dispose();
+
+ if (process.ExitCode == 0)
+ {
+ _logger.LogInformation("FFMpeg exited with code 0");
+ }
+ else
+ {
+ _logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
+ }
+
+ process.Dispose();
+ }
+
+ private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
+ {
+ if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && _isoManager.CanMount(state.MediaPath))
+ {
+ state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+
+ if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
+ {
+ var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
+ new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ _encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
+
+ if (state.VideoRequest != null)
+ {
+ _encodingHelper.TryStreamCopy(state);
+ }
+ }
+
+ if (state.MediaSource.BufferMs.HasValue)
+ {
+ await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ }
+
+ /// <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)
+ {
+ var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+
+ if (job == null)
+ {
+ return null;
+ }
+
+ OnTranscodeBeginRequest(job);
+
+ return job;
+ }
+ }
+
+ private void OnTranscodeBeginRequest(TranscodingJobDto job)
+ {
+ job.ActiveRequestCount++;
+
+ if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
+ {
+ job.StopKillTimer();
+ }
+ }
+
+ /// <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;
+ }
+
+ return result;
+ }
+ }
+
+ private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ {
+ if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
+ {
+ PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+ }
+ }
+
+ /// <summary>
+ /// Deletes the encoded media cache.
+ /// </summary>
+ private void DeleteEncodedMediaCache()
+ {
+ var path = _serverConfigurationManager.GetTranscodePath();
+ if (!Directory.Exists(path))
+ {
+ return;
+ }
+
+ foreach (var file in _fileSystem.GetFilePaths(path, true))
+ {
+ _fileSystem.DeleteFile(file);
+ }
+ }
+
+ /// <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)
+ {
+ _loggerFactory.Dispose();
+ _sessionManager!.PlaybackProgress -= OnPlaybackProgress;
+ _sessionManager!.PlaybackStart -= OnPlaybackProgress;
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 93c4612b6..24bc07b66 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -14,12 +14,15 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
</ItemGroup>
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
new file mode 100644
index 000000000..3b827ec12
--- /dev/null
+++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.ConfigurationDtos
+{
+ /// <summary>
+ /// Media Encoder Path Dto.
+ /// </summary>
+ public class MediaEncoderPathDto
+ {
+ /// <summary>
+ /// Gets or sets media encoder path.
+ /// </summary>
+ public string Path { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets media encoder path type.
+ /// </summary>
+ public string PathType { get; set; } = null!;
+ }
+}
diff --git a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
index e49a4be8a..2aa6373aa 100644
--- a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs
+++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
@@ -1,13 +1,18 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
-namespace MediaBrowser.WebDashboard.Api
+namespace Jellyfin.Api.Models
{
+ /// <summary>
+ /// The configuration page info.
+ /// </summary>
public class ConfigurationPageInfo
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
+ /// </summary>
+ /// <param name="page">Instance of <see cref="IPluginConfigurationPage"/> interface.</param>
public ConfigurationPageInfo(IPluginConfigurationPage page)
{
Name = page.Name;
@@ -22,6 +27,11 @@ namespace MediaBrowser.WebDashboard.Api
}
}
+ /// <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;
@@ -40,13 +50,25 @@ namespace MediaBrowser.WebDashboard.Api
/// <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; }
- public string MenuSection { get; set; }
+ /// <summary>
+ /// Gets or sets the menu section.
+ /// </summary>
+ public string? MenuSection { get; set; }
- public string MenuIcon { get; set; }
+ /// <summary>
+ /// Gets or sets the menu icon.
+ /// </summary>
+ public string? MenuIcon { get; set; }
- public string DisplayName { get; set; }
+ /// <summary>
+ /// Gets or sets the display name.
+ /// </summary>
+ public string? DisplayName { get; set; }
/// <summary>
/// Gets or sets the type of the configuration page.
@@ -58,6 +80,6 @@ namespace MediaBrowser.WebDashboard.Api
/// Gets or sets the plugin id.
/// </summary>
/// <value>The plugin id.</value>
- public string PluginId { get; set; }
+ public string? PluginId { get; set; }
}
}
diff --git a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
new file mode 100644
index 000000000..249d828d3
--- /dev/null
+++ b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Api.Models.DisplayPreferencesDtos
+{
+ /// <summary>
+ /// Defines the display preferences for any item that supports them (usually Folders).
+ /// </summary>
+ public class DisplayPreferencesDto
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
+ /// </summary>
+ public DisplayPreferencesDto()
+ {
+ RememberIndexing = false;
+ PrimaryImageHeight = 250;
+ PrimaryImageWidth = 250;
+ ShowBackdrop = true;
+ CustomPrefs = new Dictionary<string, string>();
+ }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public string? Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the view.
+ /// </summary>
+ /// <value>The type of the view.</value>
+ public string? ViewType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sort by.
+ /// </summary>
+ /// <value>The sort by.</value>
+ public string? SortBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index by.
+ /// </summary>
+ /// <value>The index by.</value>
+ public string? IndexBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [remember indexing].
+ /// </summary>
+ /// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value>
+ public bool RememberIndexing { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height of the primary image.
+ /// </summary>
+ /// <value>The height of the primary image.</value>
+ public int PrimaryImageHeight { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width of the primary image.
+ /// </summary>
+ /// <value>The width of the primary image.</value>
+ public int PrimaryImageWidth { get; set; }
+
+ /// <summary>
+ /// Gets the custom prefs.
+ /// </summary>
+ /// <value>The custom prefs.</value>
+ public Dictionary<string, string> CustomPrefs { get; }
+
+ /// <summary>
+ /// Gets or sets the scroll direction.
+ /// </summary>
+ /// <value>The scroll direction.</value>
+ public ScrollDirection ScrollDirection { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to show backdrops on this item.
+ /// </summary>
+ /// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value>
+ public bool ShowBackdrop { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [remember sorting].
+ /// </summary>
+ /// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value>
+ public bool RememberSorting { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sort order.
+ /// </summary>
+ /// <value>The sort order.</value>
+ public SortOrder SortOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [show sidebar].
+ /// </summary>
+ /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value>
+ public bool ShowSidebar { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client.
+ /// </summary>
+ public string? Client { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
new file mode 100644
index 000000000..92be15b8a
--- /dev/null
+++ b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Models.EnvironmentDtos
+{
+ /// <summary>
+ /// Default directory browser info.
+ /// </summary>
+ public class DefaultDirectoryBrowserInfoDto
+ {
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ public string? Path { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
new file mode 100644
index 000000000..418c11c2d
--- /dev/null
+++ b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Api.Models.EnvironmentDtos
+{
+ /// <summary>
+ /// Validate path object.
+ /// </summary>
+ public class ValidatePathDto
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether validate if path is writable.
+ /// </summary>
+ public bool ValidateWritable { 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; }
+ }
+}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
new file mode 100644
index 000000000..358434434
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+ /// <summary>
+ /// Library option info dto.
+ /// </summary>
+ public class LibraryOptionInfoDto
+ {
+ /// <summary>
+ /// Gets or sets name.
+ /// </summary>
+ public string? Name { 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
new file mode 100644
index 000000000..33eda33cb
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+ /// <summary>
+ /// Library options result dto.
+ /// </summary>
+ public class LibraryOptionsResultDto
+ {
+ /// <summary>
+ /// Gets or sets the metadata savers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataSavers", Justification = "Imported from ServiceStack")]
+ public LibraryOptionInfoDto[] MetadataSavers { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets the metadata readers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataReaders", Justification = "Imported from ServiceStack")]
+ public LibraryOptionInfoDto[] MetadataReaders { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets the subtitle fetchers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SubtitleFetchers", Justification = "Imported from ServiceStack")]
+ public LibraryOptionInfoDto[] SubtitleFetchers { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets the type options.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "TypeOptions", Justification = "Imported from ServiceStack")]
+ public LibraryTypeOptionsDto[] TypeOptions { get; set; } = null!;
+ }
+}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
new file mode 100644
index 000000000..ad031e95e
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+ /// <summary>
+ /// Library type options dto.
+ /// </summary>
+ public class LibraryTypeOptionsDto
+ {
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ public string? Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the metadata fetchers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataFetchers", Justification = "Imported from ServiceStack")]
+ public LibraryOptionInfoDto[] MetadataFetchers { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets the image fetchers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "ImageFetchers", Justification = "Imported from ServiceStack")]
+ public LibraryOptionInfoDto[] ImageFetchers { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets the supported image types.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SupportedImageTypes", Justification = "Imported from ServiceStack")]
+ public ImageType[] SupportedImageTypes { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets the default image options.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "DefaultImageOptions", Justification = "Imported from ServiceStack")]
+ public ImageOption[] DefaultImageOptions { get; set; } = null!;
+ }
+}
diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
new file mode 100644
index 000000000..991dbfc50
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
@@ -0,0 +1,19 @@
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+ /// <summary>
+ /// Media Update Info Dto.
+ /// </summary>
+ public class MediaUpdateInfoDto
+ {
+ /// <summary>
+ /// Gets or sets media path.
+ /// </summary>
+ public string? Path { 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/LibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs
new file mode 100644
index 000000000..a13cb90db
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Api.Models.LibraryStructureDto
+{
+ /// <summary>
+ /// Library options dto.
+ /// </summary>
+ public class LibraryOptionsDto
+ {
+ /// <summary>
+ /// Gets or sets library options.
+ /// </summary>
+ public LibraryOptions? LibraryOptions { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
new file mode 100644
index 000000000..f65988259
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Api.Models.LibraryStructureDto
+{
+ /// <summary>
+ /// Media Path dto.
+ /// </summary>
+ public class MediaPathDto
+ {
+ /// <summary>
+ /// Gets or sets the name of the library.
+ /// </summary>
+ [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 info.
+ /// </summary>
+ public MediaPathInfo? PathInfo { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
new file mode 100644
index 000000000..970d8acdb
--- /dev/null
+++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+
+namespace Jellyfin.Api.Models.LiveTvDtos
+{
+ /// <summary>
+ /// Channel mapping options dto.
+ /// </summary>
+ public class ChannelMappingOptionsDto
+ {
+ /// <summary>
+ /// Gets or sets list of tuner channels.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "TunerChannels", Justification = "Imported from ServiceStack")]
+ public List<TunerChannelMapping> TunerChannels { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets list of provider channels.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "ProviderChannels", Justification = "Imported from ServiceStack")]
+ public List<NameIdPair> ProviderChannels { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets list of mappings.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "Mappings", Justification = "Imported from ServiceStack")]
+ public NameValuePair[] Mappings { get; set; } = null!;
+
+ /// <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
new file mode 100644
index 000000000..d7eaab30d
--- /dev/null
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -0,0 +1,166 @@
+using System;
+
+namespace Jellyfin.Api.Models.LiveTvDtos
+{
+ /// <summary>
+ /// Get programs dto.
+ /// </summary>
+ public class GetProgramsDto
+ {
+ /// <summary>
+ /// Gets or sets the channels to return guide information for.
+ /// </summary>
+ public string? ChannelIds { get; set; }
+
+ /// <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>
+ public string? SortBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets sort Order - Ascending,Descending.
+ /// </summary>
+ public string? SortOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the genres to return guide information for.
+ /// </summary>
+ public string? Genres { get; set; }
+
+ /// <summary>
+ /// Gets or sets the genre ids to return guide information for.
+ /// </summary>
+ public string? GenreIds { get; set; }
+
+ /// <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>
+ public string? EnableImageTypes { get; set; }
+
+ /// <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>
+ public string? Fields { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
new file mode 100644
index 000000000..f797a3807
--- /dev/null
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.MediaInfo;
+
+namespace Jellyfin.Api.Models.MediaInfoDtos
+{
+ /// <summary>
+ /// Open live stream dto.
+ /// </summary>
+ public class OpenLiveStreamDto
+ {
+ /// <summary>
+ /// Gets or sets the device profile.
+ /// </summary>
+ public DeviceProfile? DeviceProfile { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device play protocols.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "SA1011:ClosingBracketsSpace", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
+ public MediaProtocol[]? DirectPlayProtocols { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
new file mode 100644
index 000000000..af5239ec2
--- /dev/null
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
@@ -0,0 +1,51 @@
+using System;
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+ /// <summary>
+ /// The notification DTO.
+ /// </summary>
+ public class NotificationDto
+ {
+ /// <summary>
+ /// Gets or sets the notification ID. Defaults to an empty string.
+ /// </summary>
+ public string Id { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the notification's user ID. Defaults to an empty string.
+ /// </summary>
+ public string UserId { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the notification date.
+ /// </summary>
+ public DateTime Date { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the notification has been read. Defaults to false.
+ /// </summary>
+ public bool IsRead { get; set; } = false;
+
+ /// <summary>
+ /// Gets or sets the notification's name. Defaults to an empty string.
+ /// </summary>
+ public string Name { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the notification's description. Defaults to an empty string.
+ /// </summary>
+ public string Description { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the notification's URL. Defaults to an empty string.
+ /// </summary>
+ public string Url { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the notification level.
+ /// </summary>
+ public NotificationLevel Level { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
new file mode 100644
index 000000000..64e92bd83
--- /dev/null
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+ /// <summary>
+ /// A list of notifications with the total record count for pagination.
+ /// </summary>
+ public class NotificationResultDto
+ {
+ /// <summary>
+ /// Gets or sets the current page of notifications.
+ /// </summary>
+ public IReadOnlyList<NotificationDto> Notifications { get; set; } = Array.Empty<NotificationDto>();
+
+ /// <summary>
+ /// Gets or sets the total number of notifications.
+ /// </summary>
+ public int TotalRecordCount { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
new file mode 100644
index 000000000..0568dea66
--- /dev/null
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
@@ -0,0 +1,20 @@
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+ /// <summary>
+ /// The notification summary DTO.
+ /// </summary>
+ public class NotificationsSummaryDto
+ {
+ /// <summary>
+ /// Gets or sets the number of unread notifications.
+ /// </summary>
+ public int UnreadCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum unread notification level.
+ /// </summary>
+ public NotificationLevel? MaxUnreadNotificationLevel { get; set; }
+ }
+}
diff --git a/MediaBrowser.Api/TranscodingJob.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index bfc311a27..b9507a4e5 100644
--- a/MediaBrowser.Api/TranscodingJob.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -1,101 +1,174 @@
-using System;
+using System;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
-using MediaBrowser.Api.Playback;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Api
+namespace Jellyfin.Api.Models.PlaybackDtos
{
/// <summary>
/// Class TranscodingJob.
/// </summary>
- public class TranscodingJob
+ public class TranscodingJobDto
{
/// <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;
+ }
+
+ /// <summary>
/// Gets or sets the play session identifier.
/// </summary>
/// <value>The play session identifier.</value>
- public string PlaySessionId { get; set; }
+ 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; }
+ 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; }
+ public MediaSourceInfo? MediaSource { get; set; }
+
+ /// <summary>
+ /// Gets or sets path.
+ /// </summary>
+ public string? Path { get; set; }
- 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; }
+ public Process? Process { get; set; }
+
+ /// <summary>
+ /// Gets logger.
+ /// </summary>
+ public ILogger<TranscodingJobDto> Logger { get; private set; }
- public ILogger 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; }
+ private Timer? KillTimer { get; set; }
- public string DeviceId { get; set; }
-
- public CancellationTokenSource CancellationTokenSource { get; set; }
+ /// <summary>
+ /// Gets or sets device id.
+ /// </summary>
+ public string? DeviceId { get; set; }
- public object ProcessLock = new object();
+ /// <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 a value indicating whether is user paused.
+ /// </summary>
public bool IsUserPaused { get; set; }
- public string Id { 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; }
- public TranscodingThrottler TranscodingThrottler { get; set; }
-
- private readonly object _timerLock = new object();
+ /// <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; }
- public TranscodingJob(ILogger logger)
- {
- Logger = logger;
- }
-
+ /// <summary>
+ /// Stop kill timer.
+ /// </summary>
public void StopKillTimer()
{
lock (_timerLock)
@@ -104,6 +177,9 @@ namespace MediaBrowser.Api
}
}
+ /// <summary>
+ /// Dispose kill timer.
+ /// </summary>
public void DisposeKillTimer()
{
lock (_timerLock)
@@ -116,11 +192,20 @@ namespace MediaBrowser.Api
}
}
+ /// <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>
+ /// <param name="intervalMs">Callback interval.</param>
public void StartKillTimer(Action<object> callback, int intervalMs)
{
if (HasExited)
@@ -143,6 +228,9 @@ namespace MediaBrowser.Api
}
}
+ /// <summary>
+ /// Change kill timer if started.
+ /// </summary>
public void ChangeKillTimerIfStarted()
{
if (HasExited)
diff --git a/MediaBrowser.Api/Playback/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index 0e73d77ef..b5e42ea29 100644
--- a/MediaBrowser.Api/Playback/TranscodingThrottler.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -6,18 +6,28 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Api.Playback
+namespace Jellyfin.Api.Models.PlaybackDtos
{
+ /// <summary>
+ /// Transcoding throttler.
+ /// </summary>
public class TranscodingThrottler : IDisposable
{
- private readonly TranscodingJob _job;
- private readonly ILogger _logger;
- private Timer _timer;
- private bool _isPaused;
+ private readonly TranscodingJobDto _job;
+ private readonly ILogger<TranscodingThrottler> _logger;
private readonly IConfigurationManager _config;
private readonly IFileSystem _fileSystem;
+ private Timer? _timer;
+ private bool _isPaused;
- public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem)
+ /// <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>
+ public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem)
{
_job = job;
_logger = logger;
@@ -25,14 +35,70 @@ namespace MediaBrowser.Api.Playback
_fileSystem = fileSystem;
}
- private EncodingOptions GetOptions()
+ /// <summary>
+ /// Start timer.
+ /// </summary>
+ public void Start()
{
- return _config.GetConfiguration<EncodingOptions>("encoding");
+ _timer = new Timer(TimerCallback, null, 5000, 5000);
}
- public void Start()
+ /// <summary>
+ /// Unpause transcoding.
+ /// </summary>
+ /// <returns>A <see cref="Task"/>.</returns>
+ public async Task UnpauseTranscoding()
{
- _timer = new Timer(TimerCallback, null, 5000, 5000);
+ if (_isPaused)
+ {
+ _logger.LogDebug("Sending resume command to ffmpeg");
+
+ try
+ {
+ await _job.Process!.StandardInput.WriteLineAsync().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()
+ {
+ 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();
+ }
+ }
+
+ private EncodingOptions GetOptions()
+ {
+ return _config.GetConfiguration<EncodingOptions>("encoding");
}
private async void TimerCallback(object state)
@@ -47,11 +113,11 @@ namespace MediaBrowser.Api.Playback
if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
{
- await PauseTranscoding();
+ await PauseTranscoding().ConfigureAwait(false);
}
else
{
- await UnpauseTranscoding();
+ await UnpauseTranscoding().ConfigureAwait(false);
}
}
@@ -63,7 +129,7 @@ namespace MediaBrowser.Api.Playback
try
{
- await _job.Process.StandardInput.WriteAsync("c");
+ await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
_isPaused = true;
}
catch (Exception ex)
@@ -73,25 +139,7 @@ namespace MediaBrowser.Api.Playback
}
}
- public async Task UnpauseTranscoding()
- {
- if (_isPaused)
- {
- _logger.LogDebug("Sending resume command to ffmpeg");
-
- try
- {
- await _job.Process.StandardInput.WriteLineAsync();
- _isPaused = false;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error resuming transcoding");
- }
- }
- }
-
- private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds)
+ private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds)
{
var bytesDownloaded = job.BytesDownloaded ?? 0;
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
@@ -152,17 +200,6 @@ namespace MediaBrowser.Api.Playback
return false;
}
- public async Task Stop()
- {
- DisposeTimer();
- await UnpauseTranscoding();
- }
-
- public void Dispose()
- {
- DisposeTimer();
- }
-
private void DisposeTimer()
{
if (_timer != null)
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
new file mode 100644
index 000000000..0d67c86f7
--- /dev/null
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Jellyfin.Api.Models.PlaylistDtos
+{
+ /// <summary>
+ /// Create new playlist dto.
+ /// </summary>
+ public class CreatePlaylistDto
+ {
+ /// <summary>
+ /// Gets or sets the name of the new playlist.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets item ids to add to the playlist.
+ /// </summary>
+ public string? Ids { 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; }
+ }
+}
diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
new file mode 100644
index 000000000..7f1255f4b
--- /dev/null
+++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace Jellyfin.Api.Models.PluginDtos
+{
+ /// <summary>
+ /// MB Registration Record.
+ /// </summary>
+ public class MBRegistrationRecord
+ {
+ /// <summary>
+ /// Gets or sets expiration date.
+ /// </summary>
+ public DateTime ExpirationDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether is registered.
+ /// </summary>
+ public bool IsRegistered { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether reg checked.
+ /// </summary>
+ public bool RegChecked { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether reg error.
+ /// </summary>
+ public bool RegError { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether trial version.
+ /// </summary>
+ public bool TrialVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether is valid.
+ /// </summary>
+ public bool IsValid { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
new file mode 100644
index 000000000..a90398425
--- /dev/null
+++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.PluginDtos
+{
+ /// <summary>
+ /// Plugin security info.
+ /// </summary>
+ public class PluginSecurityInfo
+ {
+ /// <summary>
+ /// Gets or sets the supporter key.
+ /// </summary>
+ public string? SupporterKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether is mb supporter.
+ /// </summary>
+ public bool IsMbSupporter { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
index 5a83a030d..a5f012245 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace Jellyfin.Api.Models.StartupDtos
{
/// <summary>
@@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos
/// <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; }
+ public string? MetadataCountryCode { get; set; }
/// <summary>
/// Gets or sets the preferred language for the metadata.
/// </summary>
- public string PreferredMetadataLanguage { get; set; }
+ public string? PreferredMetadataLanguage { get; set; }
}
}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
index 0dbb245ec..e4c973548 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace Jellyfin.Api.Models.StartupDtos
{
/// <summary>
@@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos
/// <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; }
+ public string? Password { get; set; }
}
}
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
new file mode 100644
index 000000000..3791fadbe
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+ /// <summary>
+ /// The hls video request dto.
+ /// </summary>
+ public class HlsAudioRequestDto : StreamingRequestDto
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
+ /// </summary>
+ public bool EnableAdaptiveBitrateStreaming { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
new file mode 100644
index 000000000..7a4be091b
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+ /// <summary>
+ /// The hls video request dto.
+ /// </summary>
+ public class HlsVideoRequestDto : VideoRequestDto
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
+ /// </summary>
+ public bool EnableAdaptiveBitrateStreaming { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
new file mode 100644
index 000000000..e95f2d1f4
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -0,0 +1,201 @@
+using System;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+
+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>
+ /// Gets or sets the request.
+ /// </summary>
+ public StreamingRequestDto Request
+ {
+ get => (StreamingRequestDto)BaseRequest;
+ set
+ {
+ BaseRequest = value;
+ IsVideoRequest = VideoRequest != null;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the transcoding throttler.
+ /// </summary>
+ public TranscodingThrottler? TranscodingThrottler { get; set; }
+
+ /// <summary>
+ /// Gets the video request.
+ /// </summary>
+ public VideoRequestDto? VideoRequest => Request! as VideoRequestDto;
+
+ /// <summary>
+ /// Gets or sets the direct stream provicer.
+ /// </summary>
+ 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 (Request.SegmentLength.HasValue)
+ {
+ return Request.SegmentLength.Value;
+ }
+
+ if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
+ {
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Gets the minimum number of segments.
+ /// </summary>
+ public int MinSegments
+ {
+ get
+ {
+ if (Request.MinSegments.HasValue)
+ {
+ return Request.MinSegments.Value;
+ }
+
+ 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; }
+
+ /// <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 beeing disposed.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ 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();
+ }
+
+ TranscodingThrottler?.Dispose();
+ }
+
+ TranscodingThrottler = null;
+ TranscodingJob = null;
+
+ _disposed = true;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
new file mode 100644
index 000000000..1791b0370
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.MediaEncoding;
+
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+ /// <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; }
+ }
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
new file mode 100644
index 000000000..cce2a89d4
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
@@ -0,0 +1,19 @@
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+ /// <summary>
+ /// The video request dto.
+ /// </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;
+
+ /// <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/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
new file mode 100644
index 000000000..393627435
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+ /// <summary>
+ /// The authenticate user by name request body.
+ /// </summary>
+ public class AuthenticateUserByName
+ {
+ /// <summary>
+ /// Gets or sets the username.
+ /// </summary>
+ public string? Username { get; set; }
+
+ /// <summary>
+ /// Gets or sets the plain text password.
+ /// </summary>
+ public string? Pw { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sha1-hashed password.
+ /// </summary>
+ public string? Password { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
new file mode 100644
index 000000000..1c88d3628
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+ /// <summary>
+ /// The create user by name request body.
+ /// </summary>
+ public class CreateUserByName
+ {
+ /// <summary>
+ /// Gets or sets the username.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the password.
+ /// </summary>
+ public string? Password { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
new file mode 100644
index 000000000..0a173ea1a
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+ /// <summary>
+ /// The update user easy password request body.
+ /// </summary>
+ public class UpdateUserEasyPassword
+ {
+ /// <summary>
+ /// Gets or sets the new sha1-hashed password.
+ /// </summary>
+ public string? NewPassword { 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; }
+ }
+}
diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs
new file mode 100644
index 000000000..8288dbbc4
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs
@@ -0,0 +1,28 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+ /// <summary>
+ /// The update user password request body.
+ /// </summary>
+ public class UpdateUserPassword
+ {
+ /// <summary>
+ /// Gets or sets the current sha1-hashed password.
+ /// </summary>
+ 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 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; }
+ }
+}
diff --git a/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
new file mode 100644
index 000000000..84b6b0958
--- /dev/null
+++ b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.UserViewDtos
+{
+ /// <summary>
+ /// Special view option dto.
+ /// </summary>
+ public class SpecialViewOptionDto
+ {
+ /// <summary>
+ /// Gets or sets view option name.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets view option id.
+ /// </summary>
+ public string? Id { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs
new file mode 100644
index 000000000..db55dc34b
--- /dev/null
+++ b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Dlna;
+
+namespace Jellyfin.Api.Models.VideoDtos
+{
+ /// <summary>
+ /// Device profile dto.
+ /// </summary>
+ public class DeviceProfileDto
+ {
+ /// <summary>
+ /// Gets or sets device profile.
+ /// </summary>
+ public DeviceProfile? DeviceProfile { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs b/Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs
new file mode 100644
index 000000000..315b47329
--- /dev/null
+++ b/Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Jellyfin.Api.TypeConverters
+{
+ /// <summary>
+ /// Custom datetime parser.
+ /// </summary>
+ public class DateTimeTypeConverter : TypeConverter
+ {
+ /// <inheritdoc />
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ if (sourceType == typeof(string))
+ {
+ return true;
+ }
+
+ return base.CanConvertFrom(context, sourceType);
+ }
+
+ /// <inheritdoc />
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ if (value is string dateString)
+ {
+ // Mark Played Item.
+ if (DateTime.TryParseExact(dateString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime))
+ {
+ return dateTime;
+ }
+
+ // Get Activity Logs.
+ if (DateTime.TryParse(dateString, null, DateTimeStyles.RoundtripKind, out dateTime))
+ {
+ return dateTime;
+ }
+ }
+
+ return base.ConvertFrom(context, culture, value);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 39976371a..6395b8d62 100644
--- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Events;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Api.System
+namespace Jellyfin.Api.WebSocketListeners
{
/// <summary>
/// Class SessionInfoWebSocketListener.
@@ -13,26 +13,27 @@ namespace MediaBrowser.Api.System
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
{
/// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- protected override string Name => "ActivityLogEntry";
-
- /// <summary>
/// The _kernel.
/// </summary>
private readonly IActivityManager _activityManager;
- public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
+ /// <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;
}
- private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
- {
- SendData(true);
- }
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ protected override string Name => "ActivityLogEntry";
/// <summary>
/// Gets the data to send.
@@ -50,5 +51,10 @@ namespace MediaBrowser.Api.System
base.Dispose(dispose);
}
+
+ private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+ {
+ SendData(true);
+ }
}
}
diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 25dd39f2d..12f815ff7 100644
--- a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Api.ScheduledTasks
+namespace Jellyfin.Api.WebSocketListeners
{
/// <summary>
/// Class ScheduledTasksWebSocketListener.
@@ -17,42 +17,27 @@ namespace MediaBrowser.Api.ScheduledTasks
/// Gets or sets the task manager.
/// </summary>
/// <value>The task manager.</value>
- private ITaskManager TaskManager { get; set; }
+ private readonly ITaskManager _taskManager;
/// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- protected override string Name => "ScheduledTasksInfo";
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener" /> class.
+ /// 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 = taskManager;
- TaskManager.TaskExecuting += TaskManager_TaskExecuting;
- TaskManager.TaskCompleted += TaskManager_TaskCompleted;
+ _taskManager.TaskExecuting += OnTaskExecuting;
+ _taskManager.TaskCompleted += OnTaskCompleted;
}
- void TaskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
- {
- SendData(true);
- e.Task.TaskProgress -= Argument_TaskProgress;
- }
-
- void TaskManager_TaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
- {
- SendData(true);
- e.Argument.TaskProgress += Argument_TaskProgress;
- }
-
- void Argument_TaskProgress(object sender, GenericEventArgs<double> e)
- {
- SendData(false);
- }
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ protected override string Name => "ScheduledTasksInfo";
/// <summary>
/// Gets the data to send.
@@ -60,18 +45,36 @@ namespace MediaBrowser.Api.ScheduledTasks
/// <returns>Task{IEnumerable{TaskInfo}}.</returns>
protected override Task<IEnumerable<TaskInfo>> GetDataToSend()
{
- return Task.FromResult(TaskManager.ScheduledTasks
+ 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 -= TaskManager_TaskExecuting;
- TaskManager.TaskCompleted -= TaskManager_TaskCompleted;
+ _taskManager.TaskExecuting -= OnTaskExecuting;
+ _taskManager.TaskCompleted -= OnTaskCompleted;
base.Dispose(dispose);
}
+
+ private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+ {
+ SendData(true);
+ e.Task.TaskProgress -= OnTaskProgress;
+ }
+
+ private void OnTaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
+ {
+ SendData(true);
+ e.Argument.TaskProgress += OnTaskProgress;
+ }
+
+ private void OnTaskProgress(object sender, GenericEventArgs<double> e)
+ {
+ SendData(false);
+ }
}
}
diff --git a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 2400d6def..1fb5dc412 100644
--- a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -5,27 +5,20 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Api.Sessions
+namespace Jellyfin.Api.WebSocketListeners
{
/// <summary>
/// Class SessionInfoWebSocketListener.
/// </summary>
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
{
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- protected override string Name => "Sessions";
-
- /// <summary>
- /// The _kernel.
- /// </summary>
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)
{
@@ -40,6 +33,32 @@ namespace MediaBrowser.Api.Sessions
_sessionManager.SessionActivity += OnSessionManagerSessionActivity;
}
+ /// <inheritdoc />
+ protected override string Name => "Sessions";
+
+ /// <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);
@@ -74,28 +93,5 @@ namespace MediaBrowser.Api.Sessions
{
await SendData(true).ConfigureAwait(false);
}
-
- /// <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);
- }
}
}
diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
new file mode 100644
index 000000000..cda83f6bb
--- /dev/null
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+ /// <summary>
+ /// An entity representing a user's display preferences.
+ /// </summary>
+ public class DisplayPreferences
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
+ /// </summary>
+ /// <param name="userId">The user's id.</param>
+ /// <param name="client">The client string.</param>
+ public DisplayPreferences(Guid userId, string client)
+ {
+ UserId = userId;
+ Client = client;
+ ShowSidebar = false;
+ ShowBackdrop = true;
+ SkipForwardLength = 30000;
+ SkipBackwardLength = 10000;
+ ScrollDirection = ScrollDirection.Horizontal;
+ ChromecastVersion = ChromecastVersion.Stable;
+ DashboardTheme = string.Empty;
+ TvHome = string.Empty;
+
+ HomeSections = new HashSet<HomeSection>();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
+ /// </summary>
+ protected DisplayPreferences()
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Gets or sets the user Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client string.
+ /// </summary>
+ /// <remarks>
+ /// Required. Max Length = 32.
+ /// </remarks>
+ [Required]
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to show the sidebar.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool ShowSidebar { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to show the backdrop.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool ShowBackdrop { get; set; }
+
+ /// <summary>
+ /// Gets or sets the scroll direction.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public ScrollDirection ScrollDirection { get; set; }
+
+ /// <summary>
+ /// Gets or sets what the view should be indexed by.
+ /// </summary>
+ public IndexingKind? IndexBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the length of time to skip forwards, in milliseconds.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int SkipForwardLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the length of time to skip backwards, in milliseconds.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int SkipBackwardLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Chromecast Version.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public ChromecastVersion ChromecastVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the next video info overlay should be shown.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool EnableNextVideoInfoOverlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the dashboard theme.
+ /// </summary>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string DashboardTheme { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tv home screen.
+ /// </summary>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string TvHome { get; set; }
+
+ /// <summary>
+ /// Gets or sets the home sections.
+ /// </summary>
+ public virtual ICollection<HomeSection> HomeSections { get; protected set; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs
new file mode 100644
index 000000000..062046260
--- /dev/null
+++ b/Jellyfin.Data/Entities/HomeSection.cs
@@ -0,0 +1,46 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+ /// <summary>
+ /// An entity representing a section on the user's home page.
+ /// </summary>
+ public class HomeSection
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity. Required.
+ /// </remarks>
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Gets or sets the Id of the associated display preferences.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int DisplayPreferencesId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the order.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Order { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public HomeSectionType Type { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
new file mode 100644
index 000000000..95c08e6c6
--- /dev/null
+++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
@@ -0,0 +1,120 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+ public class ItemDisplayPreferences
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client.</param>
+ public ItemDisplayPreferences(Guid userId, Guid itemId, string client)
+ {
+ UserId = userId;
+ ItemId = itemId;
+ Client = client;
+
+ SortBy = "SortName";
+ ViewType = ViewType.Poster;
+ SortOrder = SortOrder.Ascending;
+ RememberSorting = false;
+ RememberIndexing = false;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
+ /// </summary>
+ protected ItemDisplayPreferences()
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Gets or sets the user Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client string.
+ /// </summary>
+ /// <remarks>
+ /// Required. Max Length = 32.
+ /// </remarks>
+ [Required]
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets the view type.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public ViewType ViewType { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the indexing should be remembered.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool RememberIndexing { get; set; }
+
+ /// <summary>
+ /// Gets or sets what the view should be indexed by.
+ /// </summary>
+ public IndexingKind? IndexBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the sorting type should be remembered.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool RememberSorting { get; set; }
+
+ /// <summary>
+ /// Gets or sets what the view should be sorted by.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string SortBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sort order.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public SortOrder SortOrder { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index b89b0a8f4..50810561f 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -48,6 +48,7 @@ namespace Jellyfin.Data.Entities
PasswordResetProviderId = passwordResetProviderId;
AccessSchedules = new HashSet<AccessSchedule>();
+ ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
// Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
Preferences = new HashSet<Preference>();
@@ -327,6 +328,15 @@ namespace Jellyfin.Data.Entities
// [ForeignKey("UserId")]
public virtual ImageInfo ProfileImage { get; set; }
+ /// <summary>
+ /// Gets or sets the user's display preferences.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public virtual DisplayPreferences DisplayPreferences { get; set; }
+
[Required]
public SyncPlayAccess SyncPlayAccess { get; set; }
@@ -349,6 +359,11 @@ namespace Jellyfin.Data.Entities
/// </summary>
public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
+ /// <summary>
+ /// Gets or sets the list of item display preferences.
+ /// </summary>
+ public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; protected set; }
+
/*
/// <summary>
/// Gets or sets the list of groups this user is a member of.
diff --git a/Jellyfin.Data/Enums/ChromecastVersion.cs b/Jellyfin.Data/Enums/ChromecastVersion.cs
new file mode 100644
index 000000000..2622e08ef
--- /dev/null
+++ b/Jellyfin.Data/Enums/ChromecastVersion.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+ /// <summary>
+ /// An enum representing the version of Chromecast to be used by clients.
+ /// </summary>
+ public enum ChromecastVersion
+ {
+ /// <summary>
+ /// Stable Chromecast version.
+ /// </summary>
+ Stable = 0,
+
+ /// <summary>
+ /// Unstable Chromecast version.
+ /// </summary>
+ Unstable = 1
+ }
+}
diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Data/Enums/HomeSectionType.cs
new file mode 100644
index 000000000..e597c9431
--- /dev/null
+++ b/Jellyfin.Data/Enums/HomeSectionType.cs
@@ -0,0 +1,53 @@
+namespace Jellyfin.Data.Enums
+{
+ /// <summary>
+ /// An enum representing the different options for the home screen sections.
+ /// </summary>
+ public enum HomeSectionType
+ {
+ /// <summary>
+ /// None.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// My Media.
+ /// </summary>
+ SmallLibraryTiles = 1,
+
+ /// <summary>
+ /// My Media Small.
+ /// </summary>
+ LibraryButtons = 2,
+
+ /// <summary>
+ /// Active Recordings.
+ /// </summary>
+ ActiveRecordings = 3,
+
+ /// <summary>
+ /// Continue Watching.
+ /// </summary>
+ Resume = 4,
+
+ /// <summary>
+ /// Continue Listening.
+ /// </summary>
+ ResumeAudio = 5,
+
+ /// <summary>
+ /// Latest Media.
+ /// </summary>
+ LatestMedia = 6,
+
+ /// <summary>
+ /// Next Up.
+ /// </summary>
+ NextUp = 7,
+
+ /// <summary>
+ /// Live TV.
+ /// </summary>
+ LiveTv = 8
+ }
+}
diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Data/Enums/IndexingKind.cs
new file mode 100644
index 000000000..9badc6573
--- /dev/null
+++ b/Jellyfin.Data/Enums/IndexingKind.cs
@@ -0,0 +1,20 @@
+namespace Jellyfin.Data.Enums
+{
+ public enum IndexingKind
+ {
+ /// <summary>
+ /// Index by the premiere date.
+ /// </summary>
+ PremiereDate = 0,
+
+ /// <summary>
+ /// Index by the production year.
+ /// </summary>
+ ProductionYear = 1,
+
+ /// <summary>
+ /// Index by the community rating.
+ /// </summary>
+ CommunityRating = 2
+ }
+}
diff --git a/Jellyfin.Data/Enums/ScrollDirection.cs b/Jellyfin.Data/Enums/ScrollDirection.cs
new file mode 100644
index 000000000..9595eb490
--- /dev/null
+++ b/Jellyfin.Data/Enums/ScrollDirection.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+ /// <summary>
+ /// An enum representing the axis that should be scrolled.
+ /// </summary>
+ public enum ScrollDirection
+ {
+ /// <summary>
+ /// Horizontal scrolling direction.
+ /// </summary>
+ Horizontal = 0,
+
+ /// <summary>
+ /// Vertical scrolling direction.
+ /// </summary>
+ Vertical = 1
+ }
+}
diff --git a/Jellyfin.Data/Enums/SortOrder.cs b/Jellyfin.Data/Enums/SortOrder.cs
new file mode 100644
index 000000000..760a857f5
--- /dev/null
+++ b/Jellyfin.Data/Enums/SortOrder.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+ /// <summary>
+ /// An enum representing the sorting order.
+ /// </summary>
+ public enum SortOrder
+ {
+ /// <summary>
+ /// Sort in increasing order.
+ /// </summary>
+ Ascending = 0,
+
+ /// <summary>
+ /// Sort in decreasing order.
+ /// </summary>
+ Descending = 1
+ }
+}
diff --git a/Jellyfin.Data/Enums/ViewType.cs b/Jellyfin.Data/Enums/ViewType.cs
new file mode 100644
index 000000000..595429ab1
--- /dev/null
+++ b/Jellyfin.Data/Enums/ViewType.cs
@@ -0,0 +1,38 @@
+namespace Jellyfin.Data.Enums
+{
+ /// <summary>
+ /// An enum representing the type of view for a library or collection.
+ /// </summary>
+ public enum ViewType
+ {
+ /// <summary>
+ /// Shows banners.
+ /// </summary>
+ Banner = 0,
+
+ /// <summary>
+ /// Shows a list of content.
+ /// </summary>
+ List = 1,
+
+ /// <summary>
+ /// Shows poster artwork.
+ /// </summary>
+ Poster = 2,
+
+ /// <summary>
+ /// Shows poster artwork with a card containing the name and year.
+ /// </summary>
+ PosterCard = 3,
+
+ /// <summary>
+ /// Shows a thumbnail.
+ /// </summary>
+ Thumb = 4,
+
+ /// <summary>
+ /// Shows a thumbnail with a card containing the name and year.
+ /// </summary>
+ ThumbCard = 5
+ }
+}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 282ea511c..8ce0f3848 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -19,9 +19,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.5" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.6" />
</ItemGroup>
</Project>
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 6db514b2e..c71c76f08 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -18,11 +18,10 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BlurHashSharp" Version="1.0.1" />
- <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.0.0" />
- <PackageReference Include="SkiaSharp" Version="1.68.3" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
- <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
+ <PackageReference Include="BlurHashSharp" Version="1.1.0" />
+ <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
+ <PackageReference Include="SkiaSharp" Version="2.80.1" />
+ <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
index f2df066ec..6136a2ff9 100644
--- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
+++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
@@ -19,22 +19,18 @@ namespace Jellyfin.Drawing.Skia
/// <param name="percent">The percentage played to display with the indicator.</param>
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
{
- using (var paint = new SKPaint())
- {
- var endX = imageSize.Width - 1;
- var endY = imageSize.Height - 1;
+ using var paint = new SKPaint();
+ var endX = imageSize.Width - 1;
+ var endY = imageSize.Height - 1;
- paint.Color = SKColor.Parse("#99000000");
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint);
+ paint.Color = SKColor.Parse("#99000000");
+ paint.Style = SKPaintStyle.Fill;
+ canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
- double foregroundWidth = endX;
- foregroundWidth *= percent;
- foregroundWidth /= 100;
+ double foregroundWidth = (endX * percent) / 100;
- paint.Color = SKColor.Parse("#FF00A4DC");
- canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
- }
+ paint.Color = SKColor.Parse("#FF00A4DC");
+ canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
}
}
}
diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
index 7eed5f4f7..db4f78398 100644
--- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
+++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
@@ -22,31 +22,27 @@ namespace Jellyfin.Drawing.Skia
{
var x = imageSize.Width - OffsetFromTopRightCorner;
- using (var paint = new SKPaint())
+ using var paint = new SKPaint
{
- paint.Color = SKColor.Parse("#CC00A4DC");
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- }
+ Color = SKColor.Parse("#CC00A4DC"),
+ Style = SKPaintStyle.Fill
+ };
- using (var paint = new SKPaint())
- {
- paint.Color = new SKColor(255, 255, 255, 255);
- paint.Style = SKPaintStyle.Fill;
+ canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- paint.TextSize = 30;
- paint.IsAntialias = true;
+ 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);
+ // 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);
+ // ask the font manager for a font with that character
+ paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
- canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
- }
+ canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
}
}
}
diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs
index 1d2db5515..9a50a4d62 100644
--- a/Jellyfin.Drawing.Skia/SkiaCodecException.cs
+++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.Drawing.Skia
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
/// </summary>
/// <param name="result">The non-successful codec result returned by Skia.</param>
- public SkiaCodecException(SKCodecResult result) : base()
+ public SkiaCodecException(SKCodecResult result)
{
CodecResult = result;
}
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index ba9a5809f..a1caa751b 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -29,9 +29,7 @@ namespace Jellyfin.Drawing.Skia
/// </summary>
/// <param name="logger">The application logger.</param>
/// <param name="appPaths">The application paths.</param>
- public SkiaEncoder(
- ILogger<SkiaEncoder> logger,
- IApplicationPaths appPaths)
+ public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
{
_logger = logger;
_appPaths = appPaths;
@@ -102,19 +100,14 @@ namespace Jellyfin.Drawing.Skia
/// <returns>The converted format.</returns>
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
{
- switch (selectedFormat)
- {
- case ImageFormat.Bmp:
- return SKEncodedImageFormat.Bmp;
- case ImageFormat.Jpg:
- return SKEncodedImageFormat.Jpeg;
- case ImageFormat.Gif:
- return SKEncodedImageFormat.Gif;
- case ImageFormat.Webp:
- return SKEncodedImageFormat.Webp;
- default:
- return SKEncodedImageFormat.Png;
- }
+ return selectedFormat switch
+ {
+ ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
+ ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
+ ImageFormat.Gif => SKEncodedImageFormat.Gif,
+ ImageFormat.Webp => SKEncodedImageFormat.Webp,
+ _ => SKEncodedImageFormat.Png
+ };
}
private static bool IsTransparentRow(SKBitmap bmp, int row)
@@ -146,63 +139,34 @@ namespace Jellyfin.Drawing.Skia
private SKBitmap CropWhiteSpace(SKBitmap bitmap)
{
var topmost = 0;
- for (int row = 0; row < bitmap.Height; ++row)
+ while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost))
{
- if (IsTransparentRow(bitmap, row))
- {
- topmost = row + 1;
- }
- else
- {
- break;
- }
+ topmost++;
}
int bottommost = bitmap.Height;
- for (int row = bitmap.Height - 1; row >= 0; --row)
+ while (bottommost >= 0 && IsTransparentRow(bitmap, bottommost - 1))
{
- if (IsTransparentRow(bitmap, row))
- {
- bottommost = row;
- }
- else
- {
- break;
- }
+ bottommost--;
}
- int leftmost = 0, rightmost = bitmap.Width;
- for (int col = 0; col < bitmap.Width; ++col)
+ var leftmost = 0;
+ while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost))
{
- if (IsTransparentColumn(bitmap, col))
- {
- leftmost = col + 1;
- }
- else
- {
- break;
- }
+ leftmost++;
}
- for (int col = bitmap.Width - 1; col >= 0; --col)
+ var rightmost = bitmap.Width;
+ while (rightmost >= 0 && IsTransparentColumn(bitmap, rightmost - 1))
{
- if (IsTransparentColumn(bitmap, col))
- {
- rightmost = col;
- }
- else
- {
- break;
- }
+ rightmost--;
}
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
- using (var image = SKImage.FromBitmap(bitmap))
- using (var subset = image.Subset(newRect))
- {
- return SKBitmap.FromImage(subset);
- }
+ using var image = SKImage.FromBitmap(bitmap);
+ using var subset = image.Subset(newRect);
+ return SKBitmap.FromImage(subset);
}
/// <inheritdoc />
@@ -216,14 +180,12 @@ namespace Jellyfin.Drawing.Skia
throw new FileNotFoundException("File not found", path);
}
- using (var codec = SKCodec.Create(path, out SKCodecResult result))
- {
- EnsureSuccess(result);
+ using var codec = SKCodec.Create(path, out SKCodecResult result);
+ EnsureSuccess(result);
- var info = codec.Info;
+ var info = codec.Info;
- return new ImageDimensions(info.Width, info.Height);
- }
+ return new ImageDimensions(info.Width, info.Height);
}
/// <inheritdoc />
@@ -237,7 +199,8 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentNullException(nameof(path));
}
- return BlurHashEncoder.Encode(xComp, yComp, path);
+ // Any larger than 128x128 is too slow and there's no visually discernible difference
+ return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
}
private static bool HasDiacritics(string text)
@@ -253,12 +216,7 @@ namespace Jellyfin.Drawing.Skia
}
}
- if (HasDiacritics(path))
- {
- return true;
- }
-
- return false;
+ return HasDiacritics(path);
}
private string NormalizePath(string path)
@@ -283,25 +241,17 @@ namespace Jellyfin.Drawing.Skia
return SKEncodedOrigin.TopLeft;
}
- switch (orientation.Value)
- {
- case ImageOrientation.TopRight:
- return SKEncodedOrigin.TopRight;
- case ImageOrientation.RightTop:
- return SKEncodedOrigin.RightTop;
- case ImageOrientation.RightBottom:
- return SKEncodedOrigin.RightBottom;
- case ImageOrientation.LeftTop:
- return SKEncodedOrigin.LeftTop;
- case ImageOrientation.LeftBottom:
- return SKEncodedOrigin.LeftBottom;
- case ImageOrientation.BottomRight:
- return SKEncodedOrigin.BottomRight;
- case ImageOrientation.BottomLeft:
- return SKEncodedOrigin.BottomLeft;
- default:
- return SKEncodedOrigin.TopLeft;
- }
+ return orientation.Value switch
+ {
+ ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
+ ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
+ ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
+ ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
+ ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
+ ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
+ ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
+ _ => SKEncodedOrigin.TopLeft
+ };
}
/// <summary>
@@ -323,24 +273,22 @@ namespace Jellyfin.Drawing.Skia
if (requiresTransparencyHack || forceCleanBitmap)
{
- using (var codec = SKCodec.Create(NormalizePath(path)))
+ using var codec = SKCodec.Create(NormalizePath(path));
+ if (codec == null)
{
- if (codec == null)
- {
- origin = GetSKEncodedOrigin(orientation);
- return null;
- }
+ origin = GetSKEncodedOrigin(orientation);
+ return null;
+ }
- // create the bitmap
- var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
+ // create the bitmap
+ var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
- // decode
- _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
+ // decode
+ _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
- origin = codec.EncodedOrigin;
+ origin = codec.EncodedOrigin;
- return bitmap;
- }
+ return bitmap;
}
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
@@ -367,15 +315,8 @@ namespace Jellyfin.Drawing.Skia
{
if (cropWhitespace)
{
- using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin))
- {
- if (bitmap == null)
- {
- return null;
- }
-
- return CropWhiteSpace(bitmap);
- }
+ using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin);
+ return bitmap == null ? null : CropWhiteSpace(bitmap);
}
return Decode(path, forceAnalyzeBitmap, orientation, out origin);
@@ -403,133 +344,105 @@ namespace Jellyfin.Drawing.Skia
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
+ if (origin == SKEncodedOrigin.Default)
+ {
+ return bitmap;
+ }
+
+ var needsFlip = origin == SKEncodedOrigin.LeftBottom
+ || origin == SKEncodedOrigin.LeftTop
+ || origin == SKEncodedOrigin.RightBottom
+ || origin == SKEncodedOrigin.RightTop;
+ var rotated = needsFlip
+ ? new SKBitmap(bitmap.Height, bitmap.Width)
+ : new SKBitmap(bitmap.Width, bitmap.Height);
+ using var surface = new SKCanvas(rotated);
+ var midX = (float)rotated.Width / 2;
+ var midY = (float)rotated.Height / 2;
+
switch (origin)
{
case SKEncodedOrigin.TopRight:
- {
- var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(rotated.Width, 0);
- surface.Scale(-1, 1);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
+ surface.Scale(-1, 1, midX, midY);
+ break;
case SKEncodedOrigin.BottomRight:
- {
- var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
- using (var surface = new SKCanvas(rotated))
- {
- float px = (float)bitmap.Width / 2;
- float py = (float)bitmap.Height / 2;
-
- surface.RotateDegrees(180, px, py);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
+ surface.RotateDegrees(180, midX, midY);
+ break;
case SKEncodedOrigin.BottomLeft:
- {
- var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
- using (var surface = new SKCanvas(rotated))
- {
- float px = (float)bitmap.Width / 2;
-
- float py = (float)bitmap.Height / 2;
-
- surface.Translate(rotated.Width, 0);
- surface.Scale(-1, 1);
-
- surface.RotateDegrees(180, px, py);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
+ surface.Scale(1, -1, midX, midY);
+ break;
case SKEncodedOrigin.LeftTop:
- {
- // TODO: Remove dual canvases, had trouble with flipping
- using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
- {
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(rotated.Width, 0);
-
- surface.RotateDegrees(90);
-
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
- using (var flippedCanvas = new SKCanvas(flippedBitmap))
- {
- flippedCanvas.Translate(flippedBitmap.Width, 0);
- flippedCanvas.Scale(-1, 1);
- flippedCanvas.DrawBitmap(rotated, 0, 0);
- }
-
- return flippedBitmap;
- }
- }
-
+ surface.Translate(0, -rotated.Height);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(-90);
+ break;
case SKEncodedOrigin.RightTop:
- {
- var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(rotated.Width, 0);
- surface.RotateDegrees(90);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
+ surface.Translate(rotated.Width, 0);
+ surface.RotateDegrees(90);
+ break;
case SKEncodedOrigin.RightBottom:
- {
- // TODO: Remove dual canvases, had trouble with flipping
- using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
- {
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(0, rotated.Height);
- surface.RotateDegrees(270);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
- using (var flippedCanvas = new SKCanvas(flippedBitmap))
- {
- flippedCanvas.Translate(flippedBitmap.Width, 0);
- flippedCanvas.Scale(-1, 1);
- flippedCanvas.DrawBitmap(rotated, 0, 0);
- }
-
- return flippedBitmap;
- }
- }
-
+ surface.Translate(rotated.Width, 0);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(90);
+ break;
case SKEncodedOrigin.LeftBottom:
- {
- var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(0, rotated.Height);
- surface.RotateDegrees(270);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
- default: return bitmap;
+ surface.Translate(0, rotated.Height);
+ surface.RotateDegrees(-90);
+ break;
}
+
+ surface.DrawBitmap(bitmap, 0, 0);
+ return rotated;
+ }
+
+ /// <summary>
+ /// Resizes an image on the CPU, by utilizing a surface and canvas.
+ ///
+ /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
+ /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
+ /// </summary>
+ /// <param name="source">The source bitmap.</param>
+ /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
+ /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
+ /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
+ /// <returns>The resized image.</returns>
+ internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
+ {
+ using var surface = SKSurface.Create(targetInfo);
+ using var canvas = surface.Canvas;
+ using var paint = new SKPaint
+ {
+ FilterQuality = SKFilterQuality.High,
+ IsAntialias = isAntialias,
+ IsDither = isDither
+ };
+
+ var kernel = new float[9]
+ {
+ 0, -.1f, 0,
+ -.1f, 1.4f, -.1f,
+ 0, -.1f, 0,
+ };
+
+ var kernelSize = new SKSizeI(3, 3);
+ var kernelOffset = new SKPointI(1, 1);
+
+ paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
+ kernelSize,
+ kernel,
+ 1f,
+ 0f,
+ kernelOffset,
+ SKShaderTileMode.Clamp,
+ false);
+
+ canvas.DrawBitmap(
+ source,
+ SKRect.Create(0, 0, source.Width, source.Height),
+ SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+ paint);
+
+ return surface.Snapshot();
}
/// <inheritdoc/>
@@ -552,97 +465,87 @@ namespace Jellyfin.Drawing.Skia
var blur = options.Blur ?? 0;
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
- using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation))
+ using var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation);
+ if (bitmap == null)
{
- if (bitmap == null)
- {
- throw new InvalidDataException($"Skia unable to read image {inputPath}");
- }
+ throw new InvalidDataException($"Skia unable to read image {inputPath}");
+ }
- var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
+ var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
- if (!options.CropWhiteSpace
- && options.HasDefaultOptions(inputPath, originalImageSize)
- && !autoOrient)
- {
- // Just spit out the original file if all the options are default
- return inputPath;
- }
+ if (!options.CropWhiteSpace
+ && options.HasDefaultOptions(inputPath, originalImageSize)
+ && !autoOrient)
+ {
+ // Just spit out the original file if all the options are default
+ return inputPath;
+ }
+
+ var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
+
+ var width = newImageSize.Width;
+ var height = newImageSize.Height;
- var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
+ // scale image (the FromImage creates a copy)
+ var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
+ using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
- var width = newImageSize.Width;
- var height = newImageSize.Height;
+ // If all we're doing is resizing then we can stop now
+ if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
+ resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
+ return outputPath;
+ }
+
+ // create bitmap to use for canvas drawing used to draw into bitmap
+ using var saveBitmap = new SKBitmap(width, height);
+ using var canvas = new SKCanvas(saveBitmap);
+ // set background color if present
+ if (hasBackgroundColor)
+ {
+ canvas.Clear(SKColor.Parse(options.BackgroundColor));
+ }
- using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
+ // Add blur if option is present
+ if (blur > 0)
+ {
+ // create image from resized bitmap to apply blur
+ using var paint = new SKPaint();
+ using var filter = SKImageFilter.CreateBlur(blur, blur);
+ paint.ImageFilter = filter;
+ canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+ }
+ else
+ {
+ // draw resized bitmap onto canvas
+ canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
+ }
+
+ // If foreground layer present then draw
+ if (hasForegroundColor)
+ {
+ if (!double.TryParse(options.ForegroundLayer, out double opacity))
{
- // scale image
- bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+ opacity = .4;
+ }
- // If all we're doing is resizing then we can stop now
- if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
- using (var outputStream = new SKFileWStream(outputPath))
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, skiaOutputFormat, quality);
- return outputPath;
- }
- }
+ canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
+ }
- // create bitmap to use for canvas drawing used to draw into bitmap
- using (var saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType))
- using (var canvas = new SKCanvas(saveBitmap))
- {
- // set background color if present
- if (hasBackgroundColor)
- {
- canvas.Clear(SKColor.Parse(options.BackgroundColor));
- }
-
- // Add blur if option is present
- if (blur > 0)
- {
- // create image from resized bitmap to apply blur
- using (var paint = new SKPaint())
- using (var filter = SKImageFilter.CreateBlur(blur, blur))
- {
- paint.ImageFilter = filter;
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
- }
- }
- else
- {
- // draw resized bitmap onto canvas
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
- }
-
- // If foreground layer present then draw
- if (hasForegroundColor)
- {
- if (!double.TryParse(options.ForegroundLayer, out double opacity))
- {
- opacity = .4;
- }
-
- canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
- }
-
- if (hasIndicator)
- {
- DrawIndicator(canvas, width, height, options);
- }
-
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
- using (var outputStream = new SKFileWStream(outputPath))
- {
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, skiaOutputFormat, quality);
- }
- }
- }
+ if (hasIndicator)
+ {
+ DrawIndicator(canvas, width, height, options);
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ using (var outputStream = new SKFileWStream(outputPath))
+ {
+ using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
+ {
+ pixmap.Encode(outputStream, skiaOutputFormat, quality);
}
}
diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs
index 968d3a244..5b272eac5 100644
--- a/Jellyfin.Drawing.Skia/SkiaException.cs
+++ b/Jellyfin.Drawing.Skia/SkiaException.cs
@@ -10,7 +10,7 @@ namespace Jellyfin.Drawing.Skia
/// <summary>
/// Initializes a new instance of the <see cref="SkiaException"/> class.
/// </summary>
- public SkiaException() : base()
+ public SkiaException()
{
}
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 61bef90ec..10bb59648 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -69,12 +69,10 @@ namespace Jellyfin.Drawing.Skia
/// <param name="height">The desired height of the collage.</param>
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
{
- using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
- using (var outputStream = new SKFileWStream(outputPath))
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
- }
+ using var bitmap = BuildSquareCollageBitmap(paths, width, height);
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+ pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
/// <summary>
@@ -86,56 +84,44 @@ namespace Jellyfin.Drawing.Skia
/// <param name="height">The desired height of the collage.</param>
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
{
- using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
- using (var outputStream = new SKFileWStream(outputPath))
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
- }
+ using var bitmap = BuildThumbCollageBitmap(paths, width, height);
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+ pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
{
var bitmap = new SKBitmap(width, height);
- using (var canvas = new SKCanvas(bitmap))
- {
- canvas.Clear(SKColors.Black);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Black);
- // number of images used in the thumbnail
- var iCount = 3;
+ // number of images used in the thumbnail
+ var iCount = 3;
- // determine sizes for each image that will composited into the final image
- var iSlice = Convert.ToInt32(width / iCount);
- int iHeight = Convert.ToInt32(height * 1.00);
- int imageIndex = 0;
- for (int i = 0; i < iCount; i++)
+ // determine sizes for each image that will composited into the final image
+ var iSlice = Convert.ToInt32(width / iCount);
+ int iHeight = Convert.ToInt32(height * 1.00);
+ int imageIndex = 0;
+ for (int i = 0; i < iCount; i++)
+ {
+ using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
+ imageIndex = newIndex;
+ if (currentBitmap == null)
{
- using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
- {
- imageIndex = newIndex;
- if (currentBitmap == null)
- {
- continue;
- }
-
- // resize to the same aspect as the original
- int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
- using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
- {
- currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
-
- // crop image
- int ix = Math.Abs((iWidth - iSlice) / 2);
- using (var image = SKImage.FromBitmap(resizeBitmap))
- using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
- {
- // draw image onto canvas
- canvas.DrawImage(subset ?? image, iSlice * i, 0);
- }
- }
- }
+ continue;
}
+
+ // resize to the same aspect as the original
+ int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
+ using var resizedImage = SkiaEncoder.ResizeImage(currentBitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
+
+ // crop image
+ int ix = Math.Abs((iWidth - iSlice) / 2);
+ using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
+ // draw image onto canvas
+ canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0);
}
return bitmap;
@@ -176,33 +162,27 @@ namespace Jellyfin.Drawing.Skia
var cellWidth = width / 2;
var cellHeight = height / 2;
- using (var canvas = new SKCanvas(bitmap))
+ using var canvas = new SKCanvas(bitmap);
+ for (var x = 0; x < 2; x++)
{
- for (var x = 0; x < 2; x++)
+ for (var y = 0; y < 2; y++)
{
- for (var y = 0; y < 2; y++)
+ using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
+ imageIndex = newIndex;
+
+ if (currentBitmap == null)
{
- using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
- {
- imageIndex = newIndex;
-
- if (currentBitmap == null)
- {
- continue;
- }
-
- using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
- {
- // scale image
- currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
- // draw this image into the strip at the next position
- var xPos = x * cellWidth;
- var yPos = y * cellHeight;
- canvas.DrawBitmap(resizedBitmap, xPos, yPos);
- }
- }
+ continue;
}
+
+ // Scale image. The FromBitmap creates a copy
+ var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
+ using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
+
+ // draw this image into the strip at the next position
+ var xPos = x * cellWidth;
+ var yPos = y * cellHeight;
+ canvas.DrawBitmap(resizedBitmap, xPos, yPos);
}
}
diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
index cf3dbde2c..58f887c96 100644
--- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
+++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
@@ -28,41 +28,37 @@ namespace Jellyfin.Drawing.Skia
var x = imageSize.Width - OffsetFromTopRightCorner;
var text = count.ToString(CultureInfo.InvariantCulture);
- using (var paint = new SKPaint())
+ using var paint = new SKPaint
{
- paint.Color = SKColor.Parse("#CC00A4DC");
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- }
-
- using (var paint = new SKPaint())
- {
- paint.Color = new SKColor(255, 255, 255, 255);
- paint.Style = SKPaintStyle.Fill;
+ Color = SKColor.Parse("#CC00A4DC"),
+ Style = SKPaintStyle.Fill
+ };
- paint.TextSize = 24;
- paint.IsAntialias = true;
+ canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- var y = OffsetFromTopRightCorner + 9;
+ paint.Color = new SKColor(255, 255, 255, 255);
+ paint.TextSize = 24;
+ paint.IsAntialias = true;
- if (text.Length == 1)
- {
- x -= 7;
- }
+ var y = OffsetFromTopRightCorner + 9;
- if (text.Length == 2)
- {
- x -= 13;
- }
- else if (text.Length >= 3)
- {
- x -= 15;
- y -= 2;
- paint.TextSize = 18;
- }
+ if (text.Length == 1)
+ {
+ x -= 7;
+ }
- canvas.DrawText(text, x, y, paint);
+ if (text.Length == 2)
+ {
+ x -= 13;
+ }
+ else if (text.Length >= 3)
+ {
+ x -= 15;
+ y -= 2;
+ paint.TextSize = 18;
}
+
+ canvas.DrawText(text, x, y, paint);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 65ceee32b..2deefbe81 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -40,8 +40,9 @@ namespace Jellyfin.Server.Implementations.Activity
/// <inheritdoc/>
public async Task CreateAsync(ActivityLog entry)
{
- using var dbContext = _provider.CreateContext();
- await dbContext.ActivityLogs.AddAsync(entry);
+ await using var dbContext = _provider.CreateContext();
+
+ dbContext.ActivityLogs.Add(entry);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index dcac1b34b..21748ca19 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -24,11 +24,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.5">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6">
<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
index 53120a763..7d864ebc6 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using System.Linq;
using Jellyfin.Data;
using Jellyfin.Data.Entities;
@@ -27,8 +28,12 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<ActivityLog> ActivityLogs { 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<Permission> Permissions { get; set; }
public virtual DbSet<Preference> Preferences { get; set; }
@@ -133,6 +138,18 @@ namespace Jellyfin.Server.Implementations
return base.SaveChanges();
}
+ /// <inheritdoc/>
+ public override void Dispose()
+ {
+ foreach (var entry in ChangeTracker.Entries())
+ {
+ entry.State = EntityState.Detached;
+ }
+
+ GC.SuppressFinalize(this);
+ base.Dispose();
+ }
+
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
index 8f5c19900..486be6053 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
@@ -1,4 +1,6 @@
using System;
+using System.IO;
+using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -10,15 +12,20 @@ namespace Jellyfin.Server.Implementations
public class JellyfinDbProvider
{
private readonly IServiceProvider _serviceProvider;
+ private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
/// </summary>
/// <param name="serviceProvider">The application's service provider.</param>
- public JellyfinDbProvider(IServiceProvider serviceProvider)
+ /// <param name="appPaths">The application paths.</param>
+ public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths)
{
_serviceProvider = serviceProvider;
- serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
+ _appPaths = appPaths;
+
+ using var jellyfinDb = CreateContext();
+ jellyfinDb.Database.Migrate();
}
/// <summary>
@@ -27,7 +34,8 @@ namespace Jellyfin.Server.Implementations
/// <returns>The newly created context.</returns>
public JellyfinDb CreateContext()
{
- return _serviceProvider.GetRequiredService<JellyfinDb>();
+ var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
+ return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
new file mode 100644
index 000000000..d44707d06
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
@@ -0,0 +1,459 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDb))]
+ [Migration("20200728005145_AddDisplayPreferences")]
+ partial class AddDisplayPreferences
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "3.1.6");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<string>("DashboardTheme")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(64);
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Permission_Permissions_Guid");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Preference_Preferences_Guid");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("EasyPassword")
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("DisplayPreferences")
+ .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("Permission_Permissions_Guid");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("Preference_Preferences_Guid");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
new file mode 100644
index 000000000..3009f0b61
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
@@ -0,0 +1,132 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddDisplayPreferences : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "DisplayPreferences",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ UserId = table.Column<Guid>(nullable: false),
+ Client = table.Column<string>(maxLength: 32, nullable: false),
+ ShowSidebar = table.Column<bool>(nullable: false),
+ ShowBackdrop = table.Column<bool>(nullable: false),
+ ScrollDirection = table.Column<int>(nullable: false),
+ IndexBy = table.Column<int>(nullable: true),
+ SkipForwardLength = table.Column<int>(nullable: false),
+ SkipBackwardLength = table.Column<int>(nullable: false),
+ ChromecastVersion = table.Column<int>(nullable: false),
+ EnableNextVideoInfoOverlay = table.Column<bool>(nullable: false),
+ DashboardTheme = table.Column<string>(maxLength: 32, nullable: true),
+ TvHome = table.Column<string>(maxLength: 32, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_DisplayPreferences", x => x.Id);
+ table.ForeignKey(
+ name: "FK_DisplayPreferences_Users_UserId",
+ column: x => x.UserId,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ItemDisplayPreferences",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ UserId = table.Column<Guid>(nullable: false),
+ ItemId = table.Column<Guid>(nullable: false),
+ Client = table.Column<string>(maxLength: 32, nullable: false),
+ ViewType = table.Column<int>(nullable: false),
+ RememberIndexing = table.Column<bool>(nullable: false),
+ IndexBy = table.Column<int>(nullable: true),
+ RememberSorting = table.Column<bool>(nullable: false),
+ SortBy = table.Column<string>(maxLength: 64, nullable: false),
+ SortOrder = table.Column<int>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ItemDisplayPreferences", x => x.Id);
+ table.ForeignKey(
+ name: "FK_ItemDisplayPreferences_Users_UserId",
+ column: x => x.UserId,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "HomeSection",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ DisplayPreferencesId = table.Column<int>(nullable: false),
+ Order = table.Column<int>(nullable: false),
+ Type = table.Column<int>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_HomeSection", x => x.Id);
+ table.ForeignKey(
+ name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId",
+ column: x => x.DisplayPreferencesId,
+ principalSchema: "jellyfin",
+ principalTable: "DisplayPreferences",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "DisplayPreferences",
+ column: "UserId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_HomeSection_DisplayPreferencesId",
+ schema: "jellyfin",
+ table: "HomeSection",
+ column: "DisplayPreferencesId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ItemDisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "ItemDisplayPreferences",
+ column: "UserId");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "HomeSection",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "ItemDisplayPreferences",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "DisplayPreferences",
+ schema: "jellyfin");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 51fad8224..a6e6a2324 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "3.1.4");
+ .HasAnnotation("ProductVersion", "3.1.6");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -88,6 +88,82 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ActivityLogs");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<string>("DashboardTheme")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
@@ -113,6 +189,50 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ImageInfos");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(64);
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
@@ -282,6 +402,24 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsRequired();
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("DisplayPreferences")
+ .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
@@ -289,6 +427,15 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
index 94b582cde..f79e433a6 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using MediaBrowser.Common;
using MediaBrowser.Common.Cryptography;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.Cryptography;
@@ -117,29 +116,5 @@ namespace Jellyfin.Server.Implementations.Users
return Task.CompletedTask;
}
-
- /// <inheritdoc />
- public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
- {
- if (newPassword != null)
- {
- newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString();
- }
-
- if (string.IsNullOrWhiteSpace(newPasswordHash))
- {
- throw new ArgumentNullException(nameof(newPasswordHash));
- }
-
- user.EasyPassword = newPasswordHash;
- }
-
- /// <inheritdoc />
- public string? GetEasyPasswordHash(User user)
- {
- return string.IsNullOrEmpty(user.EasyPassword)
- ? null
- : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
- }
}
}
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index cf5a01f08..6cb13cd23 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -4,13 +4,14 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
+using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.Users
@@ -22,8 +23,7 @@ namespace Jellyfin.Server.Implementations.Users
{
private const string BaseResetFileName = "passwordreset";
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IUserManager _userManager;
+ private readonly IApplicationHost _appHost;
private readonly string _passwordResetFileBase;
private readonly string _passwordResetFileBaseDir;
@@ -32,17 +32,13 @@ namespace Jellyfin.Server.Implementations.Users
/// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
- /// <param name="jsonSerializer">The JSON serializer.</param>
- /// <param name="userManager">The user manager.</param>
- public DefaultPasswordResetProvider(
- IServerConfigurationManager configurationManager,
- IJsonSerializer jsonSerializer,
- IUserManager userManager)
+ /// <param name="appHost">The application host.</param>
+ public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IApplicationHost appHost)
{
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
- _jsonSerializer = jsonSerializer;
- _userManager = userManager;
+ _appHost = appHost;
+ // TODO: Remove the circular dependency on UserManager
}
/// <inheritdoc />
@@ -54,13 +50,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
+ var userManager = _appHost.Resolve<IUserManager>();
var usersReset = new List<string>();
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
SerializablePasswordReset spr;
await using (var str = File.OpenRead(resetFile))
{
- spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
+ spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
}
if (spr.ExpirationDate < DateTime.UtcNow)
@@ -72,10 +69,10 @@ namespace Jellyfin.Server.Implementations.Users
pin.Replace("-", string.Empty, StringComparison.Ordinal),
StringComparison.InvariantCultureIgnoreCase))
{
- var resetUser = _userManager.GetUserByName(spr.UserName)
+ var resetUser = userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
- await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
+ await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
usersReset.Add(resetUser.Username);
File.Delete(resetFile);
}
@@ -116,12 +113,11 @@ namespace Jellyfin.Server.Implementations.Users
await using (FileStream fileStream = File.OpenWrite(filePath))
{
- _jsonSerializer.SerializeToStream(spr, fileStream);
+ await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
await fileStream.FlushAsync().ConfigureAwait(false);
}
user.EasyPassword = pin;
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
return new ForgotPasswordResult
{
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
new file mode 100644
index 000000000..7c5c5a3ec
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -0,0 +1,88 @@
+#pragma warning disable CA1307
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Users
+{
+ /// <summary>
+ /// Manages the storage and retrieval of display preferences through Entity Framework.
+ /// </summary>
+ public class DisplayPreferencesManager : IDisplayPreferencesManager
+ {
+ private readonly JellyfinDbProvider _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The Jellyfin db provider.</param>
+ public DisplayPreferencesManager(JellyfinDbProvider dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
+ {
+ using var dbContext = _dbProvider.CreateContext();
+ var prefs = dbContext.DisplayPreferences
+ .Include(pref => pref.HomeSections)
+ .FirstOrDefault(pref =>
+ pref.UserId == userId && string.Equals(pref.Client, client));
+
+ if (prefs == null)
+ {
+ prefs = new DisplayPreferences(userId, client);
+ dbContext.DisplayPreferences.Add(prefs);
+ }
+
+ return prefs;
+ }
+
+ /// <inheritdoc />
+ public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
+ {
+ using var dbContext = _dbProvider.CreateContext();
+ var prefs = dbContext.ItemDisplayPreferences
+ .FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client));
+
+ if (prefs == null)
+ {
+ prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
+ dbContext.ItemDisplayPreferences.Add(prefs);
+ }
+
+ return prefs;
+ }
+
+ /// <inheritdoc />
+ public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
+ {
+ using var dbContext = _dbProvider.CreateContext();
+
+ return dbContext.ItemDisplayPreferences
+ .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
+ .ToList();
+ }
+
+ /// <inheritdoc />
+ public void SaveChanges(DisplayPreferences preferences)
+ {
+ using var dbContext = _dbProvider.CreateContext();
+ dbContext.Update(preferences);
+ dbContext.SaveChanges();
+ }
+
+ /// <inheritdoc />
+ public void SaveChanges(ItemDisplayPreferences preferences)
+ {
+ using var dbContext = _dbProvider.CreateContext();
+ dbContext.Update(preferences);
+ dbContext.SaveChanges();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
index 491aba1d4..e38cd07f0 100644
--- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
@@ -34,17 +34,5 @@ namespace Jellyfin.Server.Implementations.Users
{
return Task.CompletedTask;
}
-
- /// <inheritdoc />
- public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
- {
- // Nothing here
- }
-
- /// <inheritdoc />
- public string GetEasyPasswordHash(User user)
- {
- return string.Empty;
- }
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index ae5c311bf..11402ee05 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -23,6 +23,7 @@ using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Users;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.Users
@@ -38,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users
private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly ILogger<UserManager> _logger;
-
- private IAuthenticationProvider[] _authenticationProviders = null!;
- private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
- private InvalidAuthProvider _invalidAuthProvider = null!;
- private IPasswordResetProvider[] _passwordResetProviders = null!;
- private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
+ private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
+ private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
+ private readonly InvalidAuthProvider _invalidAuthProvider;
+ private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
+ private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
@@ -68,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users
_appHost = appHost;
_imageProcessor = imageProcessor;
_logger = logger;
+
+ _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
+ _authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
+
+ _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
+ _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
+ _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
}
/// <inheritdoc/>
@@ -86,10 +93,31 @@ namespace Jellyfin.Server.Implementations.Users
public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
/// <inheritdoc/>
- public IEnumerable<User> Users => _dbProvider.CreateContext().Users;
+ public IEnumerable<User> Users
+ {
+ get
+ {
+ using var dbContext = _dbProvider.CreateContext();
+ return dbContext.Users
+ .Include(user => user.Permissions)
+ .Include(user => user.Preferences)
+ .Include(user => user.AccessSchedules)
+ .Include(user => user.ProfileImage)
+ .ToList();
+ }
+ }
/// <inheritdoc/>
- public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
+ public IEnumerable<Guid> UsersIds
+ {
+ get
+ {
+ using var dbContext = _dbProvider.CreateContext();
+ return dbContext.Users
+ .Select(user => user.Id)
+ .ToList();
+ }
+ }
/// <inheritdoc/>
public User? GetUserById(Guid id)
@@ -99,7 +127,13 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- return _dbProvider.CreateContext().Users.Find(id);
+ using var dbContext = _dbProvider.CreateContext();
+ return dbContext.Users
+ .Include(user => user.Permissions)
+ .Include(user => user.Preferences)
+ .Include(user => user.AccessSchedules)
+ .Include(user => user.ProfileImage)
+ .FirstOrDefault(user => user.Id == id);
}
/// <inheritdoc/>
@@ -110,9 +144,14 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name));
}
- // This can't use an overload with StringComparer because that would cause the query to
- // have to be evaluated client-side.
- return _dbProvider.CreateContext().Users.FirstOrDefault(u => string.Equals(u.Username, name));
+ using var dbContext = _dbProvider.CreateContext();
+ return dbContext.Users
+ .Include(user => user.Permissions)
+ .Include(user => user.Preferences)
+ .Include(user => user.AccessSchedules)
+ .Include(user => user.ProfileImage)
+ .AsEnumerable()
+ .FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
}
/// <inheritdoc/>
@@ -133,7 +172,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("The new and old names must be different.");
}
- if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
+ if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
@@ -150,7 +189,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void UpdateUser(User user)
{
- var dbContext = _dbProvider.CreateContext();
+ using var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
dbContext.SaveChanges();
}
@@ -158,34 +197,42 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task UpdateUserAsync(User user)
{
- var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
- /// <inheritdoc/>
- public User CreateUser(string name)
+ internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
{
- if (!IsValidUsername(name))
- {
- throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
- }
-
- var dbContext = _dbProvider.CreateContext();
-
// TODO: Remove after user item data is migrated.
- var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
+ var max = await dbContext.Users.AnyAsync().ConfigureAwait(false)
+ ? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
+ : 0;
- var newUser = new User(
+ return new User(
name,
_defaultAuthenticationProvider.GetType().FullName,
_defaultPasswordResetProvider.GetType().FullName)
{
InternalId = max + 1
};
+ }
+
+ /// <inheritdoc/>
+ public async Task<User> CreateUserAsync(string name)
+ {
+ if (!IsValidUsername(name))
+ {
+ throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+ }
+
+ using var dbContext = _dbProvider.CreateContext();
+
+ var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
+
dbContext.Users.Add(newUser);
- dbContext.SaveChanges();
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
@@ -195,8 +242,13 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void DeleteUser(Guid userId)
{
- var dbContext = _dbProvider.CreateContext();
- var user = dbContext.Users.Find(userId);
+ using var dbContext = _dbProvider.CreateContext();
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id == userId);
if (user == null)
{
throw new ResourceNotFoundException(nameof(userId));
@@ -273,7 +325,17 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
{
- GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1);
+ if (newPassword != null)
+ {
+ newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString();
+ }
+
+ if (string.IsNullOrWhiteSpace(newPasswordSha1))
+ {
+ throw new ArgumentNullException(nameof(newPasswordSha1));
+ }
+
+ user.EasyPassword = newPasswordSha1;
UpdateUser(user);
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
@@ -371,7 +433,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentNullException(nameof(username));
}
- var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+ var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
bool success;
IAuthenticationProvider? authenticationProvider;
@@ -399,8 +461,7 @@ namespace Jellyfin.Server.Implementations.Users
// Search the database for the user again
// the authentication provider might have created it
- user = Users
- .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+ user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
{
@@ -474,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users
}
else
{
- IncrementInvalidLoginAttemptCount(user);
+ await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
_logger.LogInformation(
"Authentication request for {UserName} has been denied (IP: {IP}).",
user.Username,
@@ -492,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users
if (user != null && isInNetwork)
{
var passwordResetProvider = GetPasswordResetProvider(user);
- return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
+ var result = await passwordResetProvider
+ .StartForgotPasswordProcess(user, isInNetwork)
+ .ConfigureAwait(false);
+
+ await UpdateUserAsync(user).ConfigureAwait(false);
+ return result;
}
return new ForgotPasswordResult
@@ -522,48 +588,32 @@ namespace Jellyfin.Server.Implementations.Users
};
}
- /// <inheritdoc/>
- public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
- {
- _authenticationProviders = authenticationProviders.ToArray();
- _passwordResetProviders = passwordResetProviders.ToArray();
-
- _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
- _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
- _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
- }
-
/// <inheritdoc />
- public void Initialize()
+ public async Task InitializeAsync()
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
- var dbContext = _dbProvider.CreateContext();
+ using var dbContext = _dbProvider.CreateContext();
- if (dbContext.Users.Any())
+ if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
{
return;
}
var defaultName = Environment.UserName;
- if (string.IsNullOrWhiteSpace(defaultName))
+ if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName))
{
defaultName = "MyJellyfinUser";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
- if (!IsValidUsername(defaultName))
- {
- throw new ArgumentException("Provided username is not valid!", defaultName);
- }
-
- var newUser = CreateUser(defaultName);
+ var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
- dbContext.Users.Update(newUser);
- dbContext.SaveChanges();
+ dbContext.Users.Add(newUser);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -599,8 +649,15 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void UpdateConfiguration(Guid userId, UserConfiguration config)
{
- var dbContext = _dbProvider.CreateContext();
- var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
+ using var dbContext = _dbProvider.CreateContext();
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id == userId)
+ ?? throw new ArgumentException("No user exists with given Id!");
+
user.SubtitleMode = config.SubtitleMode;
user.HidePlayedInLatest = config.HidePlayedInLatest;
user.EnableLocalPassword = config.EnableLocalPassword;
@@ -625,8 +682,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void UpdatePolicy(Guid userId, UserPolicy policy)
{
- var dbContext = _dbProvider.CreateContext();
- var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
+ using var dbContext = _dbProvider.CreateContext();
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id == userId)
+ ?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
@@ -689,7 +752,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void ClearProfileImage(User user)
{
- var dbContext = _dbProvider.CreateContext();
+ using var dbContext = _dbProvider.CreateContext();
dbContext.Remove(user.ProfileImage);
dbContext.SaveChanges();
}
@@ -698,8 +761,8 @@ namespace Jellyfin.Server.Implementations.Users
{
// 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
- // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
- return Regex.IsMatch(name, @"^[\w\-'._@]*$");
+ // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
+ return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
}
private IAuthenticationProvider GetAuthenticationProvider(User user)
@@ -831,7 +894,7 @@ namespace Jellyfin.Server.Implementations.Users
}
}
- private void IncrementInvalidLoginAttemptCount(User user)
+ private async Task IncrementInvalidLoginAttemptCount(User user)
{
user.InvalidLoginAttemptCount++;
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
@@ -845,7 +908,7 @@ namespace Jellyfin.Server.Implementations.Users
user.InvalidLoginAttemptCount);
}
- UpdateUser(user);
+ await UpdateUserAsync(user).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index fe07411a6..29a59e1c8 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
@@ -33,9 +34,9 @@ namespace Jellyfin.Server
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
public CoreAppHost(
- ServerApplicationPaths applicationPaths,
+ IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
- StartupOptions options,
+ IStartupOptions options,
IFileSystem fileSystem,
INetworkManager networkManager)
: base(
@@ -63,17 +64,18 @@ namespace Jellyfin.Server
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
}
- // TODO: Set up scoping and use AddDbContextPool
- serviceCollection.AddDbContext<JellyfinDb>(
- options => options
- .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")
- .UseLazyLoadingProxies(),
- ServiceLifetime.Transient);
+ // TODO: Set up scoping and use AddDbContextPool,
+ // can't register as Transient since tracking transient in GC is funky
+ // serviceCollection.AddDbContext<JellyfinDb>(
+ // options => options
+ // .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
+ // ServiceLifetime.Transient);
serviceCollection.AddSingleton<JellyfinDbProvider>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>();
+ serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
base.RegisterServices(serviceCollection);
}
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index db06eb455..745567703 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
namespace Jellyfin.Server.Extensions
@@ -11,17 +12,39 @@ namespace Jellyfin.Server.Extensions
/// Adds swagger and swagger UI to the application pipeline.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
+ /// <param name="serverConfigurationManager">The server configuration.</param>
/// <returns>The updated application builder.</returns>
- public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder)
+ public static IApplicationBuilder UseJellyfinApiSwagger(
+ this IApplicationBuilder applicationBuilder,
+ IServerConfigurationManager serverConfigurationManager)
{
- applicationBuilder.UseSwagger();
-
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
- return applicationBuilder.UseSwaggerUI(c =>
+
+ var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/');
+ if (!string.IsNullOrEmpty(baseUrl))
{
- c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1");
- });
+ baseUrl += '/';
+ }
+
+ return applicationBuilder
+ .UseSwagger(c =>
+ {
+ // Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs'
+ c.RouteTemplate = $"/{baseUrl}{{documentName}}/openapi.json";
+ })
+ .UseSwaggerUI(c =>
+ {
+ c.DocumentTitle = "Jellyfin API";
+ c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API");
+ c.RoutePrefix = $"{baseUrl}api-docs/swagger";
+ })
+ .UseReDoc(c =>
+ {
+ c.DocumentTitle = "Jellyfin API";
+ c.SpecUrl($"/{baseUrl}api-docs/openapi.json");
+ c.RoutePrefix = $"{baseUrl}api-docs/redoc";
+ });
}
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 71ef9a69a..2e2bfea68 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -1,13 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
using Jellyfin.Api;
using Jellyfin.Api.Auth;
+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.LocalAccessOrRequiresElevationPolicy;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
+using Jellyfin.Server.Formatters;
+using Jellyfin.Server.Models;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Extensions
{
@@ -23,16 +42,37 @@ namespace Jellyfin.Server.Extensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
+ 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, LocalAccessOrRequiresElevationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{
options.AddPolicy(
- Policies.RequiresElevation,
+ Policies.DefaultAuthorization,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new RequiresElevationRequirement());
+ policy.AddRequirements(new DefaultAuthorizationRequirement());
+ });
+ options.AddPolicy(
+ Policies.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,
@@ -41,6 +81,41 @@ namespace Jellyfin.Server.Extensions
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.AddPolicy(
+ Policies.RequiresElevation,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new RequiresElevationRequirement());
+ });
});
}
@@ -63,9 +138,23 @@ namespace Jellyfin.Server.Extensions
/// <returns>The MVC builder.</returns>
public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl)
{
- return serviceCollection.AddMvc(opts =>
+ return serviceCollection
+ .AddCors(options =>
+ {
+ options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
+ })
+ .Configure<ForwardedHeadersOptions>(options =>
+ {
+ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+ })
+ .AddMvc(opts =>
{
opts.UseGeneralRoutePrefix(baseUrl);
+ opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
+ opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
+
+ opts.OutputFormatters.Add(new CssOutputFormatter());
+ opts.OutputFormatters.Add(new XmlOutputFormatter());
})
// Clear app parts to avoid other assemblies being picked up
@@ -73,8 +162,20 @@ namespace Jellyfin.Server.Extensions
.AddApplicationPart(typeof(StartupController).Assembly)
.AddJsonOptions(options =>
{
- // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
- options.JsonSerializerOptions.PropertyNamingPolicy = null;
+ // Update all properties that are set in JsonDefaults
+ var jsonOptions = JsonDefaults.GetPascalCaseOptions();
+
+ // From JsonDefaults
+ options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
+ options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
+ options.JsonSerializerOptions.Converters.Clear();
+ foreach (var converter in jsonOptions.Converters)
+ {
+ options.JsonSerializerOptions.Converters.Add(converter);
+ }
+
+ // From JsonDefaults.PascalCase
+ options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
})
.AddControllersAsServices();
}
@@ -88,8 +189,101 @@ namespace Jellyfin.Server.Extensions
{
return serviceCollection.AddSwaggerGen(c =>
{
- c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
+ c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
+ c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
+ {
+ Type = SecuritySchemeType.ApiKey,
+ In = ParameterLocation.Header,
+ Name = "X-Emby-Token",
+ Description = "API key header parameter"
+ });
+
+ var securitySchemeRef = new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = AuthenticationSchemes.CustomAuthentication },
+ };
+
+ // TODO: Apply this with an operation filter instead of globally
+ // https://github.com/domaindrivendev/Swashbuckle.AspNetCore#add-security-definitions-and-requirements
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement
+ {
+ { securitySchemeRef, Array.Empty<string>() }
+ });
+
+ // Add all xml doc files to swagger generator.
+ var xmlFiles = Directory.GetFiles(
+ AppContext.BaseDirectory,
+ "*.xml",
+ SearchOption.TopDirectoryOnly);
+
+ foreach (var xmlFile in xmlFiles)
+ {
+ c.IncludeXmlComments(xmlFile);
+ }
+
+ // Order actions by route path, then by http method.
+ c.OrderActionsBy(description =>
+ $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
+
+ // Use method name as operationId
+ c.CustomOperationIds(
+ description =>
+ {
+ description.TryGetMethodInfo(out MethodInfo methodInfo);
+ // Attribute name, method name, none.
+ return description?.ActionDescriptor?.AttributeRouteInfo?.Name
+ ?? methodInfo?.Name
+ ?? null;
+ });
+
+ // TODO - remove when all types are supported in System.Text.Json
+ c.AddSwaggerTypeMappings();
});
}
+
+ private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
+ {
+ /*
+ * TODO remove when System.Text.Json supports non-string keys.
+ * Used in Jellyfin.Api.Controller.GetChannels.
+ */
+ options.MapType<Dictionary<ImageType, string>>(() =>
+ new OpenApiSchema
+ {
+ Type = "object",
+ Properties = typeof(ImageType).GetEnumNames().ToDictionary(
+ name => name,
+ name => new OpenApiSchema
+ {
+ Type = "string",
+ Format = "string"
+ })
+ });
+
+ /*
+ * Support BlurHash dictionary
+ */
+ options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
+ new OpenApiSchema
+ {
+ Type = "object",
+ Properties = typeof(ImageType).GetEnumNames().ToDictionary(
+ name => name,
+ name => new OpenApiSchema
+ {
+ Type = "object", Properties = new Dictionary<string, OpenApiSchema>
+ {
+ {
+ "string",
+ new OpenApiSchema
+ {
+ Type = "string",
+ Format = "string"
+ }
+ }
+ }
+ })
+ });
+ }
}
}
diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
new file mode 100644
index 000000000..9b347ae2c
--- /dev/null
+++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -0,0 +1,21 @@
+using MediaBrowser.Common.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.GetCamelCaseOptions())
+ {
+ SupportedMediaTypes.Clear();
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
+ }
+ }
+}
diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
new file mode 100644
index 000000000..b3771b7fe
--- /dev/null
+++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Formatters;
+
+namespace Jellyfin.Server.Formatters
+{
+ /// <summary>
+ /// Css output formatter.
+ /// </summary>
+ public class CssOutputFormatter : TextOutputFormatter
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
+ /// </summary>
+ public CssOutputFormatter()
+ {
+ SupportedMediaTypes.Add("text/css");
+
+ SupportedEncodings.Add(Encoding.UTF8);
+ SupportedEncodings.Add(Encoding.Unicode);
+ }
+
+ /// <summary>
+ /// Write context object to stream.
+ /// </summary>
+ /// <param name="context">Writer context.</param>
+ /// <param name="selectedEncoding">Unused. Writer encoding.</param>
+ /// <returns>Write stream task.</returns>
+ public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
+ {
+ return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+ }
+ }
+}
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
new file mode 100644
index 000000000..0024708ba
--- /dev/null
+++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Common.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.GetPascalCaseOptions())
+ {
+ SupportedMediaTypes.Clear();
+ // Add application/json for default formatter
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\""));
+ }
+ }
+}
diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
new file mode 100644
index 000000000..58319657d
--- /dev/null
+++ b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
@@ -0,0 +1,31 @@
+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.Add(MediaTypeNames.Text.Xml);
+ SupportedMediaTypes.Add("text/xml;charset=UTF-8");
+ SupportedEncodings.Add(Encoding.UTF8);
+ SupportedEncodings.Add(Encoding.Unicode);
+ }
+
+ /// <inheritdoc />
+ public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
+ {
+ return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 6a2d252ab..7541707d9 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -41,11 +41,11 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="prometheus-net" Version="3.6.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
- <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
+ <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
new file mode 100644
index 000000000..63effafc1
--- /dev/null
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -0,0 +1,155 @@
+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 != null)
+ {
+ return GetActualException(inner);
+ }
+
+ var inners = agg.InnerExceptions;
+ if (inners.Count > 0)
+ {
+ return GetActualException(inners[0]);
+ }
+ }
+
+ return ex;
+ }
+
+ private static int GetStatusCode(Exception ex)
+ {
+ switch (ex)
+ {
+ case ArgumentException _: return StatusCodes.Status400BadRequest;
+ case SecurityException _: return StatusCodes.Status401Unauthorized;
+ case DirectoryNotFoundException _:
+ case FileNotFoundException _:
+ case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
+ case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
+ default: return StatusCodes.Status500InternalServerError;
+ }
+ }
+
+ private string NormalizeExceptionMessage(string msg)
+ {
+ if (msg == null)
+ {
+ return string.Empty;
+ }
+
+ // Strip any information we don't want to reveal
+ return msg.Replace(
+ _configuration.ApplicationPaths.ProgramSystemPath,
+ string.Empty,
+ StringComparison.OrdinalIgnoreCase)
+ .Replace(
+ _configuration.ApplicationPaths.ProgramDataPath,
+ string.Empty,
+ StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
new file mode 100644
index 000000000..3122d92cb
--- /dev/null
+++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
@@ -0,0 +1,78 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Threading.Tasks;
+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;
+
+ private readonly bool _enableWarning;
+ private readonly long _warningThreshold;
+
+ /// <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>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ResponseTimeMiddleware(
+ RequestDelegate next,
+ ILogger<ResponseTimeMiddleware> logger,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _next = next;
+ _logger = logger;
+
+ _enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
+ _warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
+ }
+
+ /// <summary>
+ /// Invoke request.
+ /// </summary>
+ /// <param name="context">Request context.</param>
+ /// <returns>Task.</returns>
+ public async Task Invoke(HttpContext context)
+ {
+ var watch = new Stopwatch();
+ watch.Start();
+
+ context.Response.OnStarting(() =>
+ {
+ watch.Stop();
+ LogWarning(context, watch);
+ var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
+ context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
+ return Task.CompletedTask;
+ });
+
+ // Call the next delegate/middleware in the pipeline
+ await this._next(context).ConfigureAwait(false);
+ }
+
+ private void LogWarning(HttpContext context, Stopwatch watch)
+ {
+ if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold)
+ {
+ _logger.LogWarning(
+ "Slow HTTP Response from {url} to {remoteIp} in {elapsed:g} with Status Code {statusCode}",
+ context.Request.GetDisplayUrl(),
+ context.Connection.RemoteIpAddress,
+ watch.Elapsed,
+ context.Response.StatusCode);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
index 6b5780a26..c1000eede 100644
--- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs
+++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
@@ -18,6 +18,11 @@ namespace Jellyfin.Server.Migrations
public string Name { get; }
/// <summary>
+ /// Gets a value indicating whether to perform migration on a new install.
+ /// </summary>
+ public bool PerformOnNewInstall { get; }
+
+ /// <summary>
/// Execute the migration routine.
/// </summary>
public void Perform();
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index d633c554d..844dae61f 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -21,7 +21,9 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.MigrateActivityLogDb),
typeof(Routines.RemoveDuplicateExtras),
typeof(Routines.AddDefaultPluginRepository),
- typeof(Routines.MigrateUserDb)
+ typeof(Routines.MigrateUserDb),
+ typeof(Routines.ReaddDefaultPluginRepository),
+ typeof(Routines.MigrateDisplayPreferencesDb)
};
/// <summary>
@@ -43,9 +45,8 @@ namespace Jellyfin.Server.Migrations
// If startup wizard is not finished, this is a fresh install.
// Don't run any migrations, just mark all of them as applied.
logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
- migrationOptions.Applied.AddRange(migrations.Select(m => (m.Id, m.Name)));
+ migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name)));
host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
- return;
}
var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
index a9d5ad16a..f6d8c9cc0 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
@@ -33,6 +33,9 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "AddDefaultPluginRepository";
/// <inheritdoc/>
+ public bool PerformOnNewInstall => true;
+
+ /// <inheritdoc/>
public void Perform()
{
_serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo);
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
index b15e09290..6821630db 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
@@ -49,6 +49,9 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "CreateLoggingConfigHeirarchy";
/// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
public void Perform()
{
var logDirectory = _appPaths.ConfigurationDirectoryPath;
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index c18aa1629..0925a87b5 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -26,6 +26,9 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "DisableTranscodingThrottling";
/// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
public void Perform()
{
// Set EnableThrottling to false since it wasn't used before and may introduce issues
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index fb3466e13..6048160c6 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -42,6 +42,9 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "MigrateActivityLogDatabase";
/// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
public void Perform()
{
var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase)
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
new file mode 100644
index 000000000..b15ccf01e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// The migration routine for migrating the display preferences database to EF Core.
+ /// </summary>
+ public class MigrateDisplayPreferencesDb : IMigrationRoutine
+ {
+ private const string DbFilename = "displaypreferences.db";
+
+ private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly JellyfinDbProvider _provider;
+ private readonly JsonSerializerOptions _jsonOptions;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="paths">The server application paths.</param>
+ /// <param name="provider">The database provider.</param>
+ public MigrateDisplayPreferencesDb(ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
+ {
+ _logger = logger;
+ _paths = paths;
+ _provider = provider;
+ _jsonOptions = new JsonSerializerOptions();
+ _jsonOptions.Converters.Add(new JsonStringEnumConverter());
+ }
+
+ /// <inheritdoc />
+ public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
+
+ /// <inheritdoc />
+ public string Name => "MigrateDisplayPreferencesDatabase";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ HomeSectionType[] defaults =
+ {
+ HomeSectionType.SmallLibraryTiles,
+ HomeSectionType.Resume,
+ HomeSectionType.ResumeAudio,
+ HomeSectionType.LiveTv,
+ HomeSectionType.NextUp,
+ HomeSectionType.LatestMedia,
+ HomeSectionType.None,
+ };
+
+ var chromecastDict = new Dictionary<string, ChromecastVersion>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "stable", ChromecastVersion.Stable },
+ { "nightly", ChromecastVersion.Unstable },
+ { "unstable", ChromecastVersion.Unstable }
+ };
+
+ var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
+ using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
+ {
+ using var dbContext = _provider.CreateContext();
+
+ var results = connection.Query("SELECT * FROM userdisplaypreferences");
+ foreach (var result in results)
+ {
+ var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
+ var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
+ ? chromecastDict[version]
+ : ChromecastVersion.Stable;
+
+ var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString())
+ {
+ IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
+ ShowBackdrop = dto.ShowBackdrop,
+ ShowSidebar = dto.ShowSidebar,
+ ScrollDirection = dto.ScrollDirection,
+ ChromecastVersion = chromecastVersion,
+ SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
+ ? int.Parse(length, CultureInfo.InvariantCulture)
+ : 30000,
+ SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
+ ? int.Parse(length, CultureInfo.InvariantCulture)
+ : 10000,
+ EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
+ ? bool.Parse(enabled)
+ : true,
+ DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
+ TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty
+ };
+
+ for (int i = 0; i < 7; i++)
+ {
+ dto.CustomPrefs.TryGetValue("homesection" + i, out var homeSection);
+
+ displayPreferences.HomeSections.Add(new HomeSection
+ {
+ Order = i,
+ Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
+ });
+ }
+
+ var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client)
+ {
+ SortBy = dto.SortBy ?? "SortName",
+ SortOrder = dto.SortOrder,
+ RememberIndexing = dto.RememberIndexing,
+ RememberSorting = dto.RememberSorting,
+ };
+
+ dbContext.Add(defaultLibraryPrefs);
+
+ foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Ordinal)))
+ {
+ if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var itemId))
+ {
+ continue;
+ }
+
+ var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client)
+ {
+ SortBy = dto.SortBy ?? "SortName",
+ SortOrder = dto.SortOrder,
+ RememberIndexing = dto.RememberIndexing,
+ RememberSorting = dto.RememberSorting,
+ };
+
+ if (Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType))
+ {
+ libraryDisplayPreferences.ViewType = viewType;
+ }
+
+ dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences);
+ }
+
+ dbContext.Add(displayPreferences);
+ }
+
+ dbContext.SaveChanges();
+ }
+
+ try
+ {
+ File.Move(dbFilePath, dbFilePath + ".old");
+
+ var journalPath = dbFilePath + "-journal";
+ if (File.Exists(journalPath))
+ {
+ File.Move(journalPath, dbFilePath + ".old-journal");
+ }
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'");
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 2be10c708..274e6ab73 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -55,6 +55,9 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "MigrateUserDatabase";
/// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
public void Perform()
{
var dataPath = _paths.DataPath;
diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
new file mode 100644
index 000000000..b281b5cc0
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
@@ -0,0 +1,49 @@
+using System;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Updates;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// Migration to initialize system configuration with the default plugin repository.
+ /// </summary>
+ public class ReaddDefaultPluginRepository : IMigrationRoutine
+ {
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
+ {
+ Name = "Jellyfin Stable",
+ Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E");
+
+ /// <inheritdoc/>
+ public string Name => "ReaddDefaultPluginRepository";
+
+ /// <inheritdoc/>
+ public bool PerformOnNewInstall => true;
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ // Only add if repository list is empty
+ if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0)
+ {
+ _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo);
+ _serverConfigurationManager.SaveConfiguration();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
index 2ebef0241..6c26e47e1 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
@@ -30,6 +30,9 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "RemoveDuplicateExtras";
/// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
public void Perform()
{
var dataPath = _paths.DataPath;
diff --git a/Jellyfin.Server/Models/ServerCorsPolicy.cs b/Jellyfin.Server/Models/ServerCorsPolicy.cs
new file mode 100644
index 000000000..ae010c042
--- /dev/null
+++ b/Jellyfin.Server/Models/ServerCorsPolicy.cs
@@ -0,0 +1,30 @@
+using Microsoft.AspNetCore.Cors.Infrastructure;
+
+namespace Jellyfin.Server.Models
+{
+ /// <summary>
+ /// Server Cors Policy.
+ /// </summary>
+ public static class ServerCorsPolicy
+ {
+ /// <summary>
+ /// Default policy name.
+ /// </summary>
+ public const string DefaultPolicyName = "DefaultCorsPolicy";
+
+ /// <summary>
+ /// Default Policy. Allow Everything.
+ /// </summary>
+ public static readonly CorsPolicy DefaultPolicy = new CorsPolicy
+ {
+ // Allow any origin
+ Origins = { "*" },
+
+ // Allow any method
+ Methods = { "*" },
+
+ // Allow any header
+ Headers = { "*" }
+ };
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 4d898ff5e..f6ac4e2a3 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -14,9 +14,9 @@ using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking;
+using Jellyfin.Api.Controllers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Extensions;
-using MediaBrowser.WebDashboard.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
@@ -167,7 +167,7 @@ namespace Jellyfin.Server
// If hosting the web client, validate the client content path
if (startupConfig.HostWebClient())
{
- string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager);
+ string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager);
if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
{
throw new InvalidOperationException(
@@ -274,10 +274,10 @@ namespace Jellyfin.Server
var addresses = appHost.ServerConfigurationManager
.Configuration
.LocalNetworkAddresses
- .Select(appHost.NormalizeConfiguredLocalAddress)
+ .Select(x => appHost.NormalizeConfiguredLocalAddress(x))
.Where(i => i != null)
.ToHashSet();
- if (addresses.Any() && !addresses.Contains(IPAddress.Any))
+ if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any))
{
if (!addresses.Contains(IPAddress.Loopback))
{
@@ -343,6 +343,34 @@ namespace Jellyfin.Server
}
}
}
+
+ // Bind to unix socket (only on macOS and Linux)
+ if (startupConfig.UseUnixSocket() && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ var socketPath = startupConfig.GetUnixSocketPath();
+ if (string.IsNullOrEmpty(socketPath))
+ {
+ var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
+ if (xdgRuntimeDir == null)
+ {
+ // Fall back to config dir
+ socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, "socket.sock");
+ }
+ else
+ {
+ socketPath = Path.Join(xdgRuntimeDir, "jellyfin-socket");
+ }
+ }
+
+ // 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);
+ }
})
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
.UseSerilog()
@@ -565,7 +593,7 @@ namespace Jellyfin.Server
var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
if (startupConfig != null && !startupConfig.HostWebClient())
{
- inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html";
+ inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger";
}
return config
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 5f9a5c161..d0dd183c6 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,4 +1,9 @@
+using System;
+using System.ComponentModel;
+using Jellyfin.Api.TypeConverters;
using Jellyfin.Server.Extensions;
+using Jellyfin.Server.Middleware;
+using Jellyfin.Server.Models;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
@@ -41,6 +46,7 @@ namespace Jellyfin.Server
services.AddCustomAuthentication();
services.AddJellyfinApiAuthorization();
+ services.AddHttpClient();
}
/// <summary>
@@ -59,15 +65,20 @@ namespace Jellyfin.Server
app.UseDeveloperExceptionPage();
}
+ app.UseMiddleware<ExceptionMiddleware>();
+
+ app.UseMiddleware<ResponseTimeMiddleware>();
+
app.UseWebSockets();
app.UseResponseCompression();
// TODO app.UseMiddleware<WebSocketMiddleware>();
- // TODO use when old API is removed: app.UseAuthentication();
- app.UseJellyfinApiSwagger();
+ app.UseAuthentication();
+ app.UseJellyfinApiSwagger(_serverConfigurationManager);
app.UseRouting();
+ app.UseCors(ServerCorsPolicy.DefaultPolicyName);
app.UseAuthorization();
if (_serverConfigurationManager.Configuration.EnableMetrics)
{
@@ -85,6 +96,9 @@ namespace Jellyfin.Server
});
app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
+
+ // Add type descriptor for legacy datetime parsing.
+ TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
}
}
}
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs
deleted file mode 100644
index b041effb2..000000000
--- a/MediaBrowser.Api/ApiEntryPoint.cs
+++ /dev/null
@@ -1,678 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Api.Playback;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class ServerEntryPoint.
- /// </summary>
- public class ApiEntryPoint : IServerEntryPoint
- {
- /// <summary>
- /// The instance.
- /// </summary>
- public static ApiEntryPoint Instance;
-
- /// <summary>
- /// The logger.
- /// </summary>
- private ILogger<ApiEntryPoint> _logger;
-
- /// <summary>
- /// The configuration manager.
- /// </summary>
- private IServerConfigurationManager _serverConfigurationManager;
-
- private readonly ISessionManager _sessionManager;
- private readonly IFileSystem _fileSystem;
- private readonly IMediaSourceManager _mediaSourceManager;
-
- /// <summary>
- /// The active transcoding jobs.
- /// </summary>
- private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
-
- private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
- new Dictionary<string, SemaphoreSlim>();
-
- private bool _disposed = false;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="sessionManager">The session manager.</param>
- /// <param name="config">The configuration.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="mediaSourceManager">The media source manager.</param>
- public ApiEntryPoint(
- ILogger<ApiEntryPoint> logger,
- ISessionManager sessionManager,
- IServerConfigurationManager config,
- IFileSystem fileSystem,
- IMediaSourceManager mediaSourceManager)
- {
- _logger = logger;
- _sessionManager = sessionManager;
- _serverConfigurationManager = config;
- _fileSystem = fileSystem;
- _mediaSourceManager = mediaSourceManager;
-
- _sessionManager.PlaybackProgress += OnPlaybackProgress;
- _sessionManager.PlaybackStart += OnPlaybackStart;
-
- Instance = this;
- }
-
- public static string[] Split(string value, char separator, bool removeEmpty)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return Array.Empty<string>();
- }
-
- return removeEmpty
- ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
- : value.Split(separator);
- }
-
- public SemaphoreSlim GetTranscodingLock(string outputPath)
- {
- lock (_transcodingLocks)
- {
- if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
- {
- result = new SemaphoreSlim(1, 1);
- _transcodingLocks[outputPath] = result;
- }
-
- return result;
- }
- }
-
- private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
- {
- if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
- {
- PingTranscodingJob(e.PlaySessionId, e.IsPaused);
- }
- }
-
- private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
- {
- if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
- {
- PingTranscodingJob(e.PlaySessionId, e.IsPaused);
- }
- }
-
- /// <summary>
- /// Runs this instance.
- /// </summary>
- public Task RunAsync()
- {
- try
- {
- DeleteEncodedMediaCache();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error deleting encoded media cache");
- }
-
- return Task.CompletedTask;
- }
-
- /// <summary>
- /// Deletes the encoded media cache.
- /// </summary>
- private void DeleteEncodedMediaCache()
- {
- var path = _serverConfigurationManager.GetTranscodePath();
- if (!Directory.Exists(path))
- {
- return;
- }
-
- foreach (var file in _fileSystem.GetFilePaths(path, true))
- {
- _fileSystem.DeleteFile(file);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (_disposed)
- {
- return;
- }
-
- if (dispose)
- {
- // TODO: dispose
- }
-
- var jobs = _activeTranscodingJobs.ToList();
- var jobCount = jobs.Count;
-
- IEnumerable<Task> GetKillJobs()
- {
- foreach (var job in jobs)
- {
- yield return KillTranscodingJob(job, false, path => true);
- }
- }
-
- // Wait for all processes to be killed
- if (jobCount > 0)
- {
- Task.WaitAll(GetKillJobs().ToArray());
- }
-
- _activeTranscodingJobs.Clear();
- _transcodingLocks.Clear();
-
- _sessionManager.PlaybackProgress -= OnPlaybackProgress;
- _sessionManager.PlaybackStart -= OnPlaybackStart;
-
- _disposed = true;
- }
-
-
- /// <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 TranscodingJob OnTranscodeBeginning(
- string path,
- string playSessionId,
- string liveStreamId,
- string transcodingJobId,
- TranscodingJobType type,
- Process process,
- string deviceId,
- StreamState state,
- CancellationTokenSource cancellationTokenSource)
- {
- lock (_activeTranscodingJobs)
- {
- var job = new TranscodingJob(_logger)
- {
- 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;
- }
- }
-
- public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
- {
- var ticks = transcodingPosition?.Ticks;
-
- if (job != null)
- {
- job.Framerate = framerate;
- job.CompletionPercentage = percentComplete;
- job.TranscodingPositionTicks = ticks;
- job.BytesTranscoded = bytesTranscoded;
- job.BitRate = bitRate;
- }
-
- var deviceId = state.Request.DeviceId;
-
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
-
- _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),
- TranscodeReasons = state.TranscodeReasons
- });
- }
- }
-
- /// <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)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
- if (job != null)
- {
- _activeTranscodingJobs.Remove(job);
- }
- }
-
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(path);
- }
-
- if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
- {
- _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
- }
- }
-
- /// <summary>
- /// Determines whether [has active transcoding job] [the specified path].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="type">The type.</param>
- /// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
- public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
- {
- return GetTranscodingJob(path, type) != null;
- }
-
- public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
- {
- lock (_activeTranscodingJobs)
- {
- return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
- }
- }
-
- public TranscodingJob GetTranscodingJob(string playSessionId)
- {
- lock (_activeTranscodingJobs)
- {
- return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
- }
- }
-
- /// <summary>
- /// Called when [transcode begin request].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="type">The type.</param>
- public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type)
- {
- lock (_activeTranscodingJobs)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
- if (job == null)
- {
- return null;
- }
-
- OnTranscodeBeginRequest(job);
-
- return job;
- }
- }
-
- public void OnTranscodeBeginRequest(TranscodingJob job)
- {
- job.ActiveRequestCount++;
-
- if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
- {
- job.StopKillTimer();
- }
- }
-
- public void OnTranscodeEndRequest(TranscodingJob job)
- {
- job.ActiveRequestCount--;
- _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
- if (job.ActiveRequestCount <= 0)
- {
- PingTimer(job, false);
- }
- }
-
- internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
- {
- if (string.IsNullOrEmpty(playSessionId))
- {
- throw new ArgumentNullException(nameof(playSessionId));
- }
-
- _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
-
- List<TranscodingJob> 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)
- {
- _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
- job.IsUserPaused = isUserPaused.Value;
- }
-
- PingTimer(job, true);
- }
- }
-
- private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
- {
- if (job.HasExited)
- {
- job.StopKillTimer();
- return;
- }
-
- var timerDuration = 10000;
-
- if (job.Type != TranscodingJobType.Progressive)
- {
- timerDuration = 60000;
- }
-
- job.PingTimeout = timerDuration;
- job.LastPingDate = DateTime.UtcNow;
-
- // Don't start the timer for playback checkins with progressive streaming
- if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
- {
- job.StartKillTimer(OnTranscodeKillTimerStopped);
- }
- else
- {
- job.ChangeKillTimerIfStarted();
- }
- }
-
- /// <summary>
- /// Called when [transcode kill timer stopped].
- /// </summary>
- /// <param name="state">The state.</param>
- private async void OnTranscodeKillTimerStopped(object state)
- {
- var job = (TranscodingJob)state;
-
- if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
- {
- var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
-
- if (timeSinceLastPing < job.PingTimeout)
- {
- job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
- return;
- }
- }
-
- _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
- await KillTranscodingJob(job, true, path => true);
- }
-
- /// <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>
- internal 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);
- }
-
- /// <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<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
- {
- var jobs = new List<TranscodingJob>();
-
- lock (_activeTranscodingJobs)
- {
- // This is really only needed for HLS.
- // Progressive streams can stop on their own reliably
- jobs.AddRange(_activeTranscodingJobs.Where(killJob));
- }
-
- if (jobs.Count == 0)
- {
- return Task.CompletedTask;
- }
-
- IEnumerable<Task> GetKillJobs()
- {
- foreach (var job in jobs)
- {
- yield return KillTranscodingJob(job, false, deleteFiles);
- }
- }
-
- return Task.WhenAll(GetKillJobs());
- }
-
- /// <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(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
- {
- job.DisposeKillTimer();
-
- _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
- lock (_activeTranscodingJobs)
- {
- _activeTranscodingJobs.Remove(job);
-
- if (!job.CancellationTokenSource.IsCancellationRequested)
- {
- job.CancellationTokenSource.Cancel();
- }
- }
-
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(job.Path);
- }
-
- lock (job.ProcessLock)
- {
- job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
-
- var process = job.Process;
-
- var hasExited = job.HasExited;
-
- if (!hasExited)
- {
- try
- {
- _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
-
- 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)
- {
- }
- }
- }
-
- if (delete(job.Path))
- {
- await DeletePartialStreamFiles(job.Path, job.Type, 0, 1500).ConfigureAwait(false);
- }
-
- if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
- {
- try
- {
- await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
- }
- }
- }
-
- private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
- {
- if (retryCount >= 10)
- {
- return;
- }
-
- _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
-
- await Task.Delay(delayMs).ConfigureAwait(false);
-
- 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 DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
- }
- catch (Exception 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)
- {
- if (File.Exists(outputFilePath))
- {
- _fileSystem.DeleteFile(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);
- var name = Path.GetFileNameWithoutExtension(outputFilePath);
-
- var filesToDelete = _fileSystem.GetFilePaths(directory)
- .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
-
- List<Exception> exs = null;
- foreach (var file in filesToDelete)
- {
- 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);
- }
- }
-
- if (exs != null)
- {
- throw new AggregateException("Error deleting HLS files", exs);
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/Attachments/AttachmentService.cs b/MediaBrowser.Api/Attachments/AttachmentService.cs
deleted file mode 100644
index 1632ca1b0..000000000
--- a/MediaBrowser.Api/Attachments/AttachmentService.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Attachments
-{
- [Route("/Videos/{Id}/{MediaSourceId}/Attachments/{Index}", "GET", Summary = "Gets specified attachment.")]
- public class GetAttachment
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "Index", Description = "The attachment stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
- public int Index { get; set; }
- }
-
- public class AttachmentService : BaseApiService
- {
- private readonly ILibraryManager _libraryManager;
- private readonly IAttachmentExtractor _attachmentExtractor;
-
- public AttachmentService(
- ILogger<AttachmentService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ILibraryManager libraryManager,
- IAttachmentExtractor attachmentExtractor)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _libraryManager = libraryManager;
- _attachmentExtractor = attachmentExtractor;
- }
-
- public async Task<object> Get(GetAttachment request)
- {
- var (attachment, attachmentStream) = await GetAttachment(request).ConfigureAwait(false);
- var mime = string.IsNullOrWhiteSpace(attachment.MimeType) ? "application/octet-stream" : attachment.MimeType;
-
- return ResultFactory.GetResult(Request, attachmentStream, mime);
- }
-
- private Task<(MediaAttachment, Stream)> GetAttachment(GetAttachment request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- return _attachmentExtractor.GetAttachment(item,
- request.MediaSourceId,
- request.Index,
- CancellationToken.None);
- }
- }
-}
diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs
deleted file mode 100644
index 63a31a745..000000000
--- a/MediaBrowser.Api/BaseApiService.cs
+++ /dev/null
@@ -1,416 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class BaseApiService.
- /// </summary>
- public abstract class BaseApiService : IService, IRequiresRequest
- {
- public BaseApiService(
- ILogger<BaseApiService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory)
- {
- Logger = logger;
- ServerConfigurationManager = serverConfigurationManager;
- ResultFactory = httpResultFactory;
- }
-
- /// <summary>
- /// Gets the logger.
- /// </summary>
- /// <value>The logger.</value>
- protected ILogger<BaseApiService> Logger { get; }
-
- /// <summary>
- /// Gets or sets the server configuration manager.
- /// </summary>
- /// <value>The server configuration manager.</value>
- protected IServerConfigurationManager ServerConfigurationManager { get; }
-
- /// <summary>
- /// Gets the HTTP result factory.
- /// </summary>
- /// <value>The HTTP result factory.</value>
- protected IHttpResultFactory ResultFactory { get; }
-
- /// <summary>
- /// Gets or sets the request context.
- /// </summary>
- /// <value>The request context.</value>
- public IRequest Request { get; set; }
-
- public string GetHeader(string name) => Request.Headers[name];
-
- public static string[] SplitValue(string value, char delim)
- {
- return value == null
- ? Array.Empty<string>()
- : value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public static Guid[] GetGuids(string value)
- {
- if (value == null)
- {
- return Array.Empty<Guid>();
- }
-
- return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(i => new Guid(i))
- .ToArray();
- }
-
- /// <summary>
- /// To the optimized result.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="result">The result.</param>
- /// <returns>System.Object.</returns>
- protected object ToOptimizedResult<T>(T result)
- where T : class
- {
- return ResultFactory.GetResult(Request, result);
- }
-
- protected void AssertCanUpdateUser(IAuthorizationContext authContext, IUserManager userManager, Guid userId, bool restrictUserPreferences)
- {
- var auth = authContext.GetAuthorizationInfo(Request);
-
- var authenticatedUser = auth.User;
-
- // If they're going to update the record of another user, they must be an administrator
- if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
- || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
- {
- throw new SecurityException("Unauthorized access.");
- }
- }
-
- /// <summary>
- /// Gets the session.
- /// </summary>
- /// <returns>SessionInfo.</returns>
- protected SessionInfo GetSession(ISessionContext sessionContext)
- {
- var session = sessionContext.GetSession(Request);
-
- if (session == null)
- {
- throw new ArgumentException("Session not found.");
- }
-
- return session;
- }
-
- protected DtoOptions GetDtoOptions(IAuthorizationContext authContext, object request)
- {
- var options = new DtoOptions();
-
- if (request is IHasItemFields hasFields)
- {
- options.Fields = hasFields.GetItemFields();
- }
-
- if (!options.ContainsField(ItemFields.RecursiveItemCount)
- || !options.ContainsField(ItemFields.ChildCount))
- {
- var client = authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
- 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 = options.Fields.Length;
- var arr = new ItemFields[oldLen + 1];
- options.Fields.CopyTo(arr, 0);
- arr[oldLen] = ItemFields.RecursiveItemCount;
- options.Fields = arr;
- }
-
- 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 = options.Fields.Length;
- var arr = new ItemFields[oldLen + 1];
- options.Fields.CopyTo(arr, 0);
- arr[oldLen] = ItemFields.ChildCount;
- options.Fields = arr;
- }
- }
-
- if (request is IHasDtoOptions hasDtoOptions)
- {
- options.EnableImages = hasDtoOptions.EnableImages ?? true;
-
- if (hasDtoOptions.ImageTypeLimit.HasValue)
- {
- options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value;
- }
-
- if (hasDtoOptions.EnableUserData.HasValue)
- {
- options.EnableUserData = hasDtoOptions.EnableUserData.Value;
- }
-
- if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
- {
- options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
- .ToArray();
- }
- }
-
- return options;
- }
-
- protected MusicArtist GetArtist(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
- {
- if (name.IndexOf(BaseItem.SlugChar) != -1)
- {
- var result = GetItemFromSlugName<MusicArtist>(libraryManager, name, dtoOptions);
-
- if (result != null)
- {
- return result;
- }
- }
-
- return libraryManager.GetArtist(name, dtoOptions);
- }
-
- protected Studio GetStudio(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
- {
- if (name.IndexOf(BaseItem.SlugChar) != -1)
- {
- var result = GetItemFromSlugName<Studio>(libraryManager, name, dtoOptions);
-
- if (result != null)
- {
- return result;
- }
- }
-
- return libraryManager.GetStudio(name);
- }
-
- protected Genre GetGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
- {
- if (name.IndexOf(BaseItem.SlugChar) != -1)
- {
- var result = GetItemFromSlugName<Genre>(libraryManager, name, dtoOptions);
-
- if (result != null)
- {
- return result;
- }
- }
-
- return libraryManager.GetGenre(name);
- }
-
- protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
- {
- if (name.IndexOf(BaseItem.SlugChar) != -1)
- {
- var result = GetItemFromSlugName<MusicGenre>(libraryManager, name, dtoOptions);
-
- if (result != null)
- {
- return result;
- }
- }
-
- return libraryManager.GetMusicGenre(name);
- }
-
- protected Person GetPerson(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
- {
- if (name.IndexOf(BaseItem.SlugChar) != -1)
- {
- var result = GetItemFromSlugName<Person>(libraryManager, name, dtoOptions);
-
- if (result != null)
- {
- return result;
- }
- }
-
- return libraryManager.GetPerson(name);
- }
-
- private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
- where T : BaseItem, new()
- {
- var result = libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '&'),
- IncludeItemTypes = new[] { typeof(T).Name },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
-
- result ??= libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '/'),
- IncludeItemTypes = new[] { typeof(T).Name },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
-
- result ??= libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '?'),
- IncludeItemTypes = new[] { typeof(T).Name },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
-
- return result;
- }
-
- /// <summary>
- /// Gets the path segment at the specified index.
- /// </summary>
- /// <param name="index">The index of the path segment.</param>
- /// <returns>The path segment at the specified index.</returns>
- /// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
- /// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
- protected internal ReadOnlySpan<char> GetPathValue(int index)
- {
- static void ThrowIndexOutOfRangeException()
- => throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
-
- static void ThrowInvalidDataException()
- => throw new InvalidDataException("Path doesn't start with the base url.");
-
- ReadOnlySpan<char> path = Request.PathInfo;
-
- // Remove the protocol part from the url
- int pos = path.LastIndexOf("://");
- if (pos != -1)
- {
- path = path.Slice(pos + 3);
- }
-
- // Remove the query string
- pos = path.LastIndexOf('?');
- if (pos != -1)
- {
- path = path.Slice(0, pos);
- }
-
- // Remove the domain
- pos = path.IndexOf('/');
- if (pos != -1)
- {
- path = path.Slice(pos);
- }
-
- // Remove base url
- string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
- int baseUrlLen = baseUrl.Length;
- if (baseUrlLen != 0)
- {
- if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
- {
- path = path.Slice(baseUrlLen);
- }
- else
- {
- // The path doesn't start with the base url,
- // how did we get here?
- ThrowInvalidDataException();
- }
- }
-
- // Remove leading /
- path = path.Slice(1);
-
- // Backwards compatibility
- const string Emby = "emby/";
- if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
- {
- path = path.Slice(Emby.Length);
- }
-
- const string MediaBrowser = "mediabrowser/";
- if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
- {
- path = path.Slice(MediaBrowser.Length);
- }
-
- // Skip segments until we are at the right index
- for (int i = 0; i < index; i++)
- {
- pos = path.IndexOf('/');
- if (pos == -1)
- {
- ThrowIndexOutOfRangeException();
- }
-
- path = path.Slice(pos + 1);
- }
-
- // Remove the rest
- pos = path.IndexOf('/');
- if (pos != -1)
- {
- path = path.Slice(0, pos);
- }
-
- return path;
- }
-
- /// <summary>
- /// Gets the name of the item by.
- /// </summary>
- protected BaseItem GetItemByName(string name, string type, ILibraryManager libraryManager, DtoOptions dtoOptions)
- {
- if (type.Equals("Person", StringComparison.OrdinalIgnoreCase))
- {
- return GetPerson(name, libraryManager, dtoOptions);
- }
- else if (type.Equals("Artist", StringComparison.OrdinalIgnoreCase))
- {
- return GetArtist(name, libraryManager, dtoOptions);
- }
- else if (type.Equals("Genre", StringComparison.OrdinalIgnoreCase))
- {
- return GetGenre(name, libraryManager, dtoOptions);
- }
- else if (type.Equals("MusicGenre", StringComparison.OrdinalIgnoreCase))
- {
- return GetMusicGenre(name, libraryManager, dtoOptions);
- }
- else if (type.Equals("Studio", StringComparison.OrdinalIgnoreCase))
- {
- return GetStudio(name, libraryManager, dtoOptions);
- }
- else if (type.Equals("Year", StringComparison.OrdinalIgnoreCase))
- {
- return libraryManager.GetYear(int.Parse(name));
- }
-
- throw new ArgumentException("Invalid type", nameof(type));
- }
- }
-}
diff --git a/MediaBrowser.Api/BrandingService.cs b/MediaBrowser.Api/BrandingService.cs
deleted file mode 100644
index f4724e774..000000000
--- a/MediaBrowser.Api/BrandingService.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Branding;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Branding/Configuration", "GET", Summary = "Gets branding configuration")]
- public class GetBrandingOptions : IReturn<BrandingOptions>
- {
- }
-
- [Route("/Branding/Css", "GET", Summary = "Gets custom css")]
- [Route("/Branding/Css.css", "GET", Summary = "Gets custom css")]
- public class GetBrandingCss
- {
- }
-
- public class BrandingService : BaseApiService
- {
- public BrandingService(
- ILogger<BrandingService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- }
-
- public object Get(GetBrandingOptions request)
- {
- return ServerConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- }
-
- public object Get(GetBrandingCss request)
- {
- var result = ServerConfigurationManager.GetConfiguration<BrandingOptions>("branding");
-
- // When null this throws a 405 error under Mono OSX, so default to empty string
- return ResultFactory.GetResult(Request, result.CustomCss ?? string.Empty, "text/css");
- }
- }
-}
diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs
deleted file mode 100644
index 8c336b1c9..000000000
--- a/MediaBrowser.Api/ChannelService.cs
+++ /dev/null
@@ -1,340 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Channels;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Channels", "GET", Summary = "Gets available channels")]
- public class GetChannels : IReturn<QueryResult<BaseItemDto>>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "SupportsLatestItems", Description = "Optional. Filter by channels that support getting latest items.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? SupportsLatestItems { get; set; }
-
- public bool? SupportsMediaDeletion { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is favorite.
- /// </summary>
- /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
- public bool? IsFavorite { get; set; }
- }
-
- [Route("/Channels/{Id}/Features", "GET", Summary = "Gets features for a channel")]
- public class GetChannelFeatures : IReturn<ChannelFeatures>
- {
- [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Channels/Features", "GET", Summary = "Gets features for a channel")]
- public class GetAllChannelFeatures : IReturn<ChannelFeatures[]>
- {
- }
-
- [Route("/Channels/{Id}/Items", "GET", Summary = "Gets channel items")]
- public class GetChannelItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
- {
- [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "FolderId", Description = "Folder Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string FolderId { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SortOrder { get; set; }
-
- [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Filters { get; set; }
-
- [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string SortBy { get; set; }
-
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- /// <summary>
- /// Gets the filters.
- /// </summary>
- /// <returns>IEnumerable{ItemFilter}.</returns>
- public IEnumerable<ItemFilter> GetFilters()
- {
- var val = Filters;
-
- return string.IsNullOrEmpty(val)
- ? Array.Empty<ItemFilter>()
- : val.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
- }
-
- /// <summary>
- /// Gets the order by.
- /// </summary>
- /// <returns>IEnumerable{ItemSortBy}.</returns>
- public ValueTuple<string, SortOrder>[] GetOrderBy()
- {
- return BaseItemsRequest.GetOrderBy(SortBy, SortOrder);
- }
- }
-
- [Route("/Channels/Items/Latest", "GET", Summary = "Gets channel items")]
- public class GetLatestChannelItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Filters { get; set; }
-
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "ChannelIds", Description = "Optional. Specify one or more channel id's, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ChannelIds { get; set; }
-
- /// <summary>
- /// Gets the filters.
- /// </summary>
- /// <returns>IEnumerable{ItemFilter}.</returns>
- public IEnumerable<ItemFilter> GetFilters()
- {
- return string.IsNullOrEmpty(Filters)
- ? Array.Empty<ItemFilter>()
- : Filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
- }
- }
-
- [Authenticated]
- public class ChannelService : BaseApiService
- {
- private readonly IChannelManager _channelManager;
- private IUserManager _userManager;
-
- public ChannelService(
- ILogger<ChannelService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IChannelManager channelManager,
- IUserManager userManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _channelManager = channelManager;
- _userManager = userManager;
- }
-
- public object Get(GetAllChannelFeatures request)
- {
- var result = _channelManager.GetAllChannelFeatures();
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetChannelFeatures request)
- {
- var result = _channelManager.GetChannelFeatures(request.Id);
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetChannels request)
- {
- var result = _channelManager.GetChannels(new ChannelQuery
- {
- Limit = request.Limit,
- StartIndex = request.StartIndex,
- UserId = request.UserId,
- SupportsLatestItems = request.SupportsLatestItems,
- SupportsMediaDeletion = request.SupportsMediaDeletion,
- IsFavorite = request.IsFavorite
- });
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetChannelItems request)
- {
- var user = request.UserId.Equals(Guid.Empty)
- ? null
- : _userManager.GetUserById(request.UserId);
-
- var query = new InternalItemsQuery(user)
- {
- Limit = request.Limit,
- StartIndex = request.StartIndex,
- ChannelIds = new[] { new Guid(request.Id) },
- ParentId = string.IsNullOrWhiteSpace(request.FolderId) ? Guid.Empty : new Guid(request.FolderId),
- OrderBy = request.GetOrderBy(),
- DtoOptions = new Controller.Dto.DtoOptions
- {
- Fields = request.GetItemFields()
- }
- };
-
- foreach (var filter in request.GetFilters())
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- var result = await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetLatestChannelItems request)
- {
- var user = request.UserId.Equals(Guid.Empty)
- ? null
- : _userManager.GetUserById(request.UserId);
-
- var query = new InternalItemsQuery(user)
- {
- Limit = request.Limit,
- StartIndex = request.StartIndex,
- ChannelIds = (request.ChannelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToArray(),
- DtoOptions = new Controller.Dto.DtoOptions
- {
- Fields = request.GetItemFields()
- }
- };
-
- foreach (var filter in request.GetFilters())
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- var result = await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
- }
-}
diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs
deleted file mode 100644
index 19369ccca..000000000
--- a/MediaBrowser.Api/ConfigurationService.cs
+++ /dev/null
@@ -1,144 +0,0 @@
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class GetConfiguration.
- /// </summary>
- [Route("/System/Configuration", "GET", Summary = "Gets application configuration")]
- [Authenticated]
- public class GetConfiguration : IReturn<ServerConfiguration>
- {
- }
-
- [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")]
- [Authenticated(AllowBeforeStartupWizard = true)]
- public class GetNamedConfiguration
- {
- [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Key { get; set; }
- }
-
- /// <summary>
- /// Class UpdateConfiguration.
- /// </summary>
- [Route("/System/Configuration", "POST", Summary = "Updates application configuration")]
- [Authenticated(Roles = "Admin")]
- public class UpdateConfiguration : ServerConfiguration, IReturnVoid
- {
- }
-
- [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")]
- [Authenticated(Roles = "Admin")]
- public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream
- {
- [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Key { get; set; }
-
- public Stream RequestStream { get; set; }
- }
-
- [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")]
- [Authenticated(Roles = "Admin")]
- public class GetDefaultMetadataOptions : IReturn<MetadataOptions>
- {
- }
-
- [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")]
- [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
- public class UpdateMediaEncoderPath : IReturnVoid
- {
- [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Path { get; set; }
- [ApiMember(Name = "PathType", Description = "PathType", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string PathType { get; set; }
- }
-
- public class ConfigurationService : BaseApiService
- {
- /// <summary>
- /// The _json serializer.
- /// </summary>
- private readonly IJsonSerializer _jsonSerializer;
-
- /// <summary>
- /// The _configuration manager.
- /// </summary>
- private readonly IServerConfigurationManager _configurationManager;
-
- private readonly IMediaEncoder _mediaEncoder;
-
- public ConfigurationService(
- ILogger<ConfigurationService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IJsonSerializer jsonSerializer,
- IServerConfigurationManager configurationManager,
- IMediaEncoder mediaEncoder)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _jsonSerializer = jsonSerializer;
- _configurationManager = configurationManager;
- _mediaEncoder = mediaEncoder;
- }
-
- public void Post(UpdateMediaEncoderPath request)
- {
- _mediaEncoder.UpdateEncoderPath(request.Path, request.PathType);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetConfiguration request)
- {
- return ToOptimizedResult(_configurationManager.Configuration);
- }
-
- public object Get(GetNamedConfiguration request)
- {
- var result = _configurationManager.GetConfiguration(request.Key);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Posts the specified configuraiton.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(UpdateConfiguration request)
- {
- // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration
- var json = _jsonSerializer.SerializeToString(request);
-
- var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
-
- _configurationManager.ReplaceConfiguration(config);
- }
-
- public async Task Post(UpdateNamedConfiguration request)
- {
- var key = GetPathValue(2).ToString();
-
- var configurationType = _configurationManager.GetConfigurationType(key);
- var configuration = await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, configurationType).ConfigureAwait(false);
-
- _configurationManager.SaveConfiguration(key, configuration);
- }
-
- public object Get(GetDefaultMetadataOptions request)
- {
- return ToOptimizedResult(new MetadataOptions());
- }
- }
-}
diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs
deleted file mode 100644
index 18860983e..000000000
--- a/MediaBrowser.Api/Devices/DeviceService.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Devices
-{
- [Route("/Devices", "GET", Summary = "Gets all devices")]
- [Authenticated(Roles = "Admin")]
- public class GetDevices : DeviceQuery, IReturn<QueryResult<DeviceInfo>>
- {
- }
-
- [Route("/Devices/Info", "GET", Summary = "Gets info for a device")]
- [Authenticated(Roles = "Admin")]
- public class GetDeviceInfo : IReturn<DeviceInfo>
- {
- [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Devices/Options", "GET", Summary = "Gets options for a device")]
- [Authenticated(Roles = "Admin")]
- public class GetDeviceOptions : IReturn<DeviceOptions>
- {
- [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Devices", "DELETE", Summary = "Deletes a device")]
- public class DeleteDevice
- {
- [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/Devices/Options", "POST", Summary = "Updates device options")]
- [Authenticated(Roles = "Admin")]
- public class PostDeviceOptions : DeviceOptions, IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- public class DeviceService : BaseApiService
- {
- private readonly IDeviceManager _deviceManager;
- private readonly IAuthenticationRepository _authRepo;
- private readonly ISessionManager _sessionManager;
-
- public DeviceService(
- ILogger<DeviceService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IDeviceManager deviceManager,
- IAuthenticationRepository authRepo,
- ISessionManager sessionManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _deviceManager = deviceManager;
- _authRepo = authRepo;
- _sessionManager = sessionManager;
- }
-
- public void Post(PostDeviceOptions request)
- {
- _deviceManager.UpdateDeviceOptions(request.Id, request);
- }
-
- public object Get(GetDevices request)
- {
- return ToOptimizedResult(_deviceManager.GetDevices(request));
- }
-
- public object Get(GetDeviceInfo request)
- {
- return _deviceManager.GetDevice(request.Id);
- }
-
- public object Get(GetDeviceOptions request)
- {
- return _deviceManager.GetDeviceOptions(request.Id);
- }
-
- public void Delete(DeleteDevice request)
- {
- var sessions = _authRepo.Get(new AuthenticationInfoQuery
- {
- DeviceId = request.Id
- }).Items;
-
- foreach (var session in sessions)
- {
- _sessionManager.Logout(session);
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
deleted file mode 100644
index c3ed40ad3..000000000
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System.Threading;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class UpdateDisplayPreferences.
- /// </summary>
- [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
- public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "DisplayPreferencesId", Description = "DisplayPreferences Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string DisplayPreferencesId { get; set; }
-
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string UserId { get; set; }
- }
-
- [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
- public class GetDisplayPreferences : IReturn<DisplayPreferences>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string UserId { get; set; }
-
- [ApiMember(Name = "Client", Description = "Client", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Client { get; set; }
- }
-
- /// <summary>
- /// Class DisplayPreferencesService.
- /// </summary>
- [Authenticated]
- public class DisplayPreferencesService : BaseApiService
- {
- /// <summary>
- /// The _display preferences manager.
- /// </summary>
- private readonly IDisplayPreferencesRepository _displayPreferencesManager;
- /// <summary>
- /// The _json serializer.
- /// </summary>
- private readonly IJsonSerializer _jsonSerializer;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
- /// </summary>
- /// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="displayPreferencesManager">The display preferences manager.</param>
- public DisplayPreferencesService(
- ILogger<DisplayPreferencesService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IJsonSerializer jsonSerializer,
- IDisplayPreferencesRepository displayPreferencesManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _jsonSerializer = jsonSerializer;
- _displayPreferencesManager = displayPreferencesManager;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Get(GetDisplayPreferences request)
- {
- var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(UpdateDisplayPreferences request)
- {
- // Serialize to json and then back so that the core doesn't see the request dto type
- var displayPreferences = _jsonSerializer.DeserializeFromString<DisplayPreferences>(_jsonSerializer.SerializeToString(request));
-
- _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None);
- }
- }
-}
diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs
deleted file mode 100644
index 720a71025..000000000
--- a/MediaBrowser.Api/EnvironmentService.cs
+++ /dev/null
@@ -1,285 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class GetDirectoryContents.
- /// </summary>
- [Route("/Environment/DirectoryContents", "GET", Summary = "Gets the contents of a given directory in the file system")]
- public class GetDirectoryContents : IReturn<List<FileSystemEntryInfo>>
- {
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Path { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [include files].
- /// </summary>
- /// <value><c>true</c> if [include files]; otherwise, <c>false</c>.</value>
- [ApiMember(Name = "IncludeFiles", Description = "An optional filter to include or exclude files from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool IncludeFiles { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [include directories].
- /// </summary>
- /// <value><c>true</c> if [include directories]; otherwise, <c>false</c>.</value>
- [ApiMember(Name = "IncludeDirectories", Description = "An optional filter to include or exclude folders from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool IncludeDirectories { get; set; }
- }
-
- [Route("/Environment/ValidatePath", "POST", Summary = "Gets the contents of a given directory in the file system")]
- public class ValidatePath
- {
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Path { get; set; }
-
- public bool ValidateWriteable { get; set; }
-
- public bool? IsFile { get; set; }
- }
-
- [Obsolete]
- [Route("/Environment/NetworkShares", "GET", Summary = "Gets shares from a network device")]
- public class GetNetworkShares : IReturn<List<FileSystemEntryInfo>>
- {
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Path { get; set; }
- }
-
- /// <summary>
- /// Class GetDrives.
- /// </summary>
- [Route("/Environment/Drives", "GET", Summary = "Gets available drives from the server's file system")]
- public class GetDrives : IReturn<List<FileSystemEntryInfo>>
- {
- }
-
- /// <summary>
- /// Class GetNetworkComputers.
- /// </summary>
- [Route("/Environment/NetworkDevices", "GET", Summary = "Gets a list of devices on the network")]
- public class GetNetworkDevices : IReturn<List<FileSystemEntryInfo>>
- {
- }
-
- [Route("/Environment/ParentPath", "GET", Summary = "Gets the parent path of a given path")]
- public class GetParentPath : IReturn<string>
- {
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Path { get; set; }
- }
-
- public class DefaultDirectoryBrowserInfo
- {
- public string Path { get; set; }
- }
-
- [Route("/Environment/DefaultDirectoryBrowser", "GET", Summary = "Gets the parent path of a given path")]
- public class GetDefaultDirectoryBrowser : IReturn<DefaultDirectoryBrowserInfo>
- {
- }
-
- /// <summary>
- /// Class EnvironmentService.
- /// </summary>
- [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
- public class EnvironmentService : BaseApiService
- {
- private const char UncSeparator = '\\';
- private const string UncSeparatorString = "\\";
-
- /// <summary>
- /// The _network manager.
- /// </summary>
- private readonly INetworkManager _networkManager;
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EnvironmentService" /> class.
- /// </summary>
- /// <param name="networkManager">The network manager.</param>
- public EnvironmentService(
- ILogger<EnvironmentService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- INetworkManager networkManager,
- IFileSystem fileSystem)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _networkManager = networkManager;
- _fileSystem = fileSystem;
- }
-
- public void Post(ValidatePath request)
- {
- if (request.IsFile.HasValue)
- {
- if (request.IsFile.Value)
- {
- if (!File.Exists(request.Path))
- {
- throw new FileNotFoundException("File not found", request.Path);
- }
- }
- else
- {
- if (!Directory.Exists(request.Path))
- {
- throw new FileNotFoundException("File not found", request.Path);
- }
- }
- }
-
- else
- {
- if (!File.Exists(request.Path) && !Directory.Exists(request.Path))
- {
- throw new FileNotFoundException("Path not found", request.Path);
- }
-
- if (request.ValidateWriteable)
- {
- EnsureWriteAccess(request.Path);
- }
- }
- }
-
- protected void EnsureWriteAccess(string path)
- {
- var file = Path.Combine(path, Guid.NewGuid().ToString());
-
- File.WriteAllText(file, string.Empty);
- _fileSystem.DeleteFile(file);
- }
-
- public object Get(GetDefaultDirectoryBrowser request) =>
- ToOptimizedResult(new DefaultDirectoryBrowserInfo { Path = null });
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetDirectoryContents request)
- {
- var path = request.Path;
-
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(Path));
- }
-
- var networkPrefix = UncSeparatorString + UncSeparatorString;
-
- if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase)
- && path.LastIndexOf(UncSeparator) == 1)
- {
- return ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
- }
-
- return ToOptimizedResult(GetFileSystemEntries(request).ToList());
- }
-
- [Obsolete]
- public object Get(GetNetworkShares request)
- => ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetDrives request)
- {
- var result = GetDrives().ToList();
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the list that is returned when an empty path is supplied.
- /// </summary>
- /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
- private IEnumerable<FileSystemEntryInfo> GetDrives()
- {
- return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetNetworkDevices request)
- => ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-
- /// <summary>
- /// Gets the file system entries.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
- private IEnumerable<FileSystemEntryInfo> GetFileSystemEntries(GetDirectoryContents request)
- {
- var entries = _fileSystem.GetFileSystemEntries(request.Path).OrderBy(i => i.FullName).Where(i =>
- {
- var isDirectory = i.IsDirectory;
-
- if (!request.IncludeFiles && !isDirectory)
- {
- return false;
- }
-
- return request.IncludeDirectories || !isDirectory;
- });
-
- return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
- }
-
- public object Get(GetParentPath request)
- {
- var parent = Path.GetDirectoryName(request.Path);
-
- if (string.IsNullOrEmpty(parent))
- {
- // Check if unc share
- var index = request.Path.LastIndexOf(UncSeparator);
-
- if (index != -1 && request.Path.IndexOf(UncSeparator) == 0)
- {
- parent = request.Path.Substring(0, index);
-
- if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
- {
- parent = null;
- }
- }
- }
-
- return parent;
- }
- }
-}
diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs
deleted file mode 100644
index 1b736c77d..000000000
--- a/MediaBrowser.Api/FilterService.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Items/Filters", "GET", Summary = "Gets branding configuration")]
- public class GetQueryFiltersLegacy : IReturn<QueryFiltersLegacy>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ParentId { get; set; }
-
- [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string IncludeItemTypes { get; set; }
-
- [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string MediaTypes { get; set; }
-
- public string[] GetMediaTypes()
- {
- return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetIncludeItemTypes()
- {
- return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- [Route("/Items/Filters2", "GET", Summary = "Gets branding configuration")]
- public class GetQueryFilters : IReturn<QueryFilters>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ParentId { get; set; }
-
- [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string IncludeItemTypes { get; set; }
-
- [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string MediaTypes { get; set; }
-
- public string[] GetMediaTypes()
- {
- return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetIncludeItemTypes()
- {
- return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public bool? IsAiring { get; set; }
-
- public bool? IsMovie { get; set; }
-
- public bool? IsSports { get; set; }
-
- public bool? IsKids { get; set; }
-
- public bool? IsNews { get; set; }
-
- public bool? IsSeries { get; set; }
-
- public bool? Recursive { get; set; }
- }
-
- [Authenticated]
- public class FilterService : BaseApiService
- {
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
-
- public FilterService(
- ILogger<FilterService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ILibraryManager libraryManager,
- IUserManager userManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- }
-
- public object Get(GetQueryFilters request)
- {
- var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId);
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
- {
- parentItem = null;
- }
-
- var filters = new QueryFilters();
-
- var genreQuery = new InternalItemsQuery(user)
- {
- IncludeItemTypes = request.GetIncludeItemTypes(),
- DtoOptions = new Controller.Dto.DtoOptions
- {
- Fields = new ItemFields[] { },
- EnableImages = false,
- EnableUserData = false
- },
- IsAiring = request.IsAiring,
- IsMovie = request.IsMovie,
- IsSports = request.IsSports,
- IsKids = request.IsKids,
- IsNews = request.IsNews,
- IsSeries = request.IsSeries
- };
-
- // Non recursive not yet supported for library folders
- if ((request.Recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
- {
- genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
- }
- else
- {
- genreQuery.Parent = parentItem;
- }
-
- if (string.Equals(request.IncludeItemTypes, "MusicAlbum", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, "MusicVideo", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, "MusicArtist", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, "Audio", StringComparison.OrdinalIgnoreCase))
- {
- filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
- {
- Name = i.Item1.Name,
- Id = i.Item1.Id
- }).ToArray();
- }
- else
- {
- filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
- {
- Name = i.Item1.Name,
- Id = i.Item1.Id
- }).ToArray();
- }
-
- return ToOptimizedResult(filters);
- }
-
- public object Get(GetQueryFiltersLegacy request)
- {
- var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId);
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
- {
- parentItem = null;
- }
-
- var item = string.IsNullOrEmpty(request.ParentId) ?
- user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() :
- parentItem;
-
- var result = ((Folder)item).GetItemList(GetItemsQuery(request, user));
-
- var filters = GetFilters(result);
-
- return ToOptimizedResult(filters);
- }
-
- private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
- {
- var result = new QueryFiltersLegacy();
-
- result.Years = items.Select(i => i.ProductionYear ?? -1)
- .Where(i => i > 0)
- .Distinct()
- .OrderBy(i => i)
- .ToArray();
-
- result.Genres = items.SelectMany(i => i.Genres)
- .DistinctNames()
- .OrderBy(i => i)
- .ToArray();
-
- result.Tags = items
- .SelectMany(i => i.Tags)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(i => i)
- .ToArray();
-
- result.OfficialRatings = items
- .Select(i => i.OfficialRating)
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(i => i)
- .ToArray();
-
- return result;
- }
-
- private InternalItemsQuery GetItemsQuery(GetQueryFiltersLegacy request, User user)
- {
- var query = new InternalItemsQuery
- {
- User = user,
- MediaTypes = request.GetMediaTypes(),
- IncludeItemTypes = request.GetIncludeItemTypes(),
- Recursive = true,
- EnableTotalRecordCount = false,
- DtoOptions = new Controller.Dto.DtoOptions
- {
- Fields = new[] { ItemFields.Genres, ItemFields.Tags },
- EnableImages = false,
- EnableUserData = false
- }
- };
-
- return query;
- }
- }
-}
diff --git a/MediaBrowser.Api/IHasDtoOptions.cs b/MediaBrowser.Api/IHasDtoOptions.cs
deleted file mode 100644
index 33d498e8b..000000000
--- a/MediaBrowser.Api/IHasDtoOptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace MediaBrowser.Api
-{
- public interface IHasDtoOptions : IHasItemFields
- {
- bool? EnableImages { get; set; }
-
- bool? EnableUserData { get; set; }
-
- int? ImageTypeLimit { get; set; }
-
- string EnableImageTypes { get; set; }
- }
-}
diff --git a/MediaBrowser.Api/IHasItemFields.cs b/MediaBrowser.Api/IHasItemFields.cs
deleted file mode 100644
index ad4f1b489..000000000
--- a/MediaBrowser.Api/IHasItemFields.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using System.Linq;
-using MediaBrowser.Model.Querying;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Interface IHasItemFields.
- /// </summary>
- public interface IHasItemFields
- {
- /// <summary>
- /// Gets or sets the fields.
- /// </summary>
- /// <value>The fields.</value>
- string Fields { get; set; }
- }
-
- /// <summary>
- /// Class ItemFieldsExtensions.
- /// </summary>
- public static class ItemFieldsExtensions
- {
- /// <summary>
- /// Gets the item fields.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>IEnumerable{ItemFields}.</returns>
- public static ItemFields[] GetItemFields(this IHasItemFields request)
- {
- var val = request.Fields;
-
- if (string.IsNullOrEmpty(val))
- {
- return Array.Empty<ItemFields>();
- }
-
- return val.Split(',').Select(v =>
- {
- if (Enum.TryParse(v, true, out ItemFields value))
- {
- return (ItemFields?)value;
- }
-
- return null;
- }).Where(i => i.HasValue).Select(i => i.Value).ToArray();
- }
- }
-}
diff --git a/MediaBrowser.Api/Images/ImageByNameService.cs b/MediaBrowser.Api/Images/ImageByNameService.cs
deleted file mode 100644
index 2d405ac3d..000000000
--- a/MediaBrowser.Api/Images/ImageByNameService.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
- /// <summary>
- /// Class GetGeneralImage.
- /// </summary>
- [Route("/Images/General/{Name}/{Type}", "GET", Summary = "Gets a general image by name")]
- public class GetGeneralImage
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- [ApiMember(Name = "Type", Description = "Image Type (primary, backdrop, logo, etc).", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Type { get; set; }
- }
-
- /// <summary>
- /// Class GetRatingImage.
- /// </summary>
- [Route("/Images/Ratings/{Theme}/{Name}", "GET", Summary = "Gets a rating image by name")]
- public class GetRatingImage
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the theme.
- /// </summary>
- /// <value>The theme.</value>
- [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Theme { get; set; }
- }
-
- /// <summary>
- /// Class GetMediaInfoImage.
- /// </summary>
- [Route("/Images/MediaInfo/{Theme}/{Name}", "GET", Summary = "Gets a media info image by name")]
- public class GetMediaInfoImage
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the theme.
- /// </summary>
- /// <value>The theme.</value>
- [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Theme { get; set; }
- }
-
- [Route("/Images/MediaInfo", "GET", Summary = "Gets all media info image by name")]
- [Authenticated]
- public class GetMediaInfoImages : IReturn<List<ImageByNameInfo>>
- {
- }
-
- [Route("/Images/Ratings", "GET", Summary = "Gets all rating images by name")]
- [Authenticated]
- public class GetRatingImages : IReturn<List<ImageByNameInfo>>
- {
- }
-
- [Route("/Images/General", "GET", Summary = "Gets all general images by name")]
- [Authenticated]
- public class GetGeneralImages : IReturn<List<ImageByNameInfo>>
- {
- }
-
- /// <summary>
- /// Class ImageByNameService.
- /// </summary>
- public class ImageByNameService : BaseApiService
- {
- /// <summary>
- /// The _app paths.
- /// </summary>
- private readonly IServerApplicationPaths _appPaths;
-
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ImageByNameService" /> class.
- /// </summary>
- public ImageByNameService(
- ILogger<ImageByNameService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory resultFactory,
- IFileSystem fileSystem)
- : base(logger, serverConfigurationManager, resultFactory)
- {
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _fileSystem = fileSystem;
- }
-
- public object Get(GetMediaInfoImages request)
- {
- return ToOptimizedResult(GetImageList(_appPaths.MediaInfoImagesPath, true));
- }
-
- public object Get(GetRatingImages request)
- {
- return ToOptimizedResult(GetImageList(_appPaths.RatingsPath, true));
- }
-
- public object Get(GetGeneralImages request)
- {
- return ToOptimizedResult(GetImageList(_appPaths.GeneralPath, false));
- }
-
- private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
- {
- try
- {
- return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
- .Select(i => new ImageByNameInfo
- {
- Name = _fileSystem.GetFileNameWithoutExtension(i),
- FileLength = i.Length,
-
- // For themeable images, use the Theme property
- // For general images, the same object structure is fine,
- // but it's not owned by a theme, so call it Context
- Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
- Context = supportsThemes ? null : GetThemeName(i.FullName, path),
-
- Format = i.Extension.ToLowerInvariant().TrimStart('.')
- })
- .OrderBy(i => i.Name)
- .ToList();
- }
- catch (IOException)
- {
- return new List<ImageByNameInfo>();
- }
- }
-
- private string GetThemeName(string path, string rootImagePath)
- {
- var parentName = Path.GetDirectoryName(path);
-
- if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
- {
- return null;
- }
-
- parentName = Path.GetFileName(parentName);
-
- return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ?
- null :
- parentName;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetGeneralImage request)
- {
- var filename = string.Equals(request.Type, "primary", StringComparison.OrdinalIgnoreCase)
- ? "folder"
- : request.Type;
-
- var paths = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(_appPaths.GeneralPath, request.Name, filename + i)).ToList();
-
- var path = paths.FirstOrDefault(File.Exists) ?? paths.FirstOrDefault();
-
- return ResultFactory.GetStaticFileResult(Request, path);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetRatingImage request)
- {
- var themeFolder = Path.Combine(_appPaths.RatingsPath, request.Theme);
-
- if (Directory.Exists(themeFolder))
- {
- var path = BaseItem.SupportedImageExtensions
- .Select(i => Path.Combine(themeFolder, request.Name + i))
- .FirstOrDefault(File.Exists);
-
- if (!string.IsNullOrEmpty(path))
- {
- return ResultFactory.GetStaticFileResult(Request, path);
- }
- }
-
- var allFolder = Path.Combine(_appPaths.RatingsPath, "all");
-
- if (Directory.Exists(allFolder))
- {
- // Avoid implicitly captured closure
- var currentRequest = request;
-
- var path = BaseItem.SupportedImageExtensions
- .Select(i => Path.Combine(allFolder, currentRequest.Name + i))
- .FirstOrDefault(File.Exists);
-
- if (!string.IsNullOrEmpty(path))
- {
- return ResultFactory.GetStaticFileResult(Request, path);
- }
- }
-
- throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetMediaInfoImage request)
- {
- var themeFolder = Path.Combine(_appPaths.MediaInfoImagesPath, request.Theme);
-
- if (Directory.Exists(themeFolder))
- {
- var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, request.Name + i))
- .FirstOrDefault(File.Exists);
-
- if (!string.IsNullOrEmpty(path))
- {
- return ResultFactory.GetStaticFileResult(Request, path);
- }
- }
-
- var allFolder = Path.Combine(_appPaths.MediaInfoImagesPath, "all");
-
- if (Directory.Exists(allFolder))
- {
- // Avoid implicitly captured closure
- var currentRequest = request;
-
- var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
- .FirstOrDefault(File.Exists);
-
- if (!string.IsNullOrEmpty(path))
- {
- return ResultFactory.GetStaticFileResult(Request, path);
- }
- }
-
- throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name);
- }
- }
-}
diff --git a/MediaBrowser.Api/Images/ImageRequest.cs b/MediaBrowser.Api/Images/ImageRequest.cs
deleted file mode 100644
index 0f3455548..000000000
--- a/MediaBrowser.Api/Images/ImageRequest.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.Images
-{
- /// <summary>
- /// Class ImageRequest.
- /// </summary>
- public class ImageRequest : DeleteImageRequest
- {
- /// <summary>
- /// The max width.
- /// </summary>
- [ApiMember(Name = "MaxWidth", Description = "The maximum image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MaxWidth { get; set; }
-
- /// <summary>
- /// The max height.
- /// </summary>
- [ApiMember(Name = "MaxHeight", Description = "The maximum image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MaxHeight { get; set; }
-
- /// <summary>
- /// The width.
- /// </summary>
- [ApiMember(Name = "Width", Description = "The fixed image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Width { get; set; }
-
- /// <summary>
- /// The height.
- /// </summary>
- [ApiMember(Name = "Height", Description = "The fixed image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Height { get; set; }
-
- /// <summary>
- /// Gets or sets the quality.
- /// </summary>
- /// <value>The quality.</value>
- [ApiMember(Name = "Quality", Description = "Optional quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Quality { get; set; }
-
- /// <summary>
- /// Gets or sets the tag.
- /// </summary>
- /// <value>The tag.</value>
- [ApiMember(Name = "Tag", Description = "Optional. Supply the cache tag from the item object to receive strong caching headers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Tag { get; set; }
-
- [ApiMember(Name = "CropWhitespace", Description = "Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? CropWhitespace { get; set; }
-
- [ApiMember(Name = "EnableImageEnhancers", Description = "Enable or disable image enhancers such as cover art.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool EnableImageEnhancers { get; set; }
-
- [ApiMember(Name = "Format", Description = "Determines the output foramt of the image - original,gif,jpg,png", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public string Format { get; set; }
-
- [ApiMember(Name = "AddPlayedIndicator", Description = "Optional. Add a played indicator", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool AddPlayedIndicator { get; set; }
-
- [ApiMember(Name = "PercentPlayed", Description = "Optional percent to render for the percent played overlay", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public double? PercentPlayed { get; set; }
-
- [ApiMember(Name = "UnplayedCount", Description = "Optional unplayed count overlay to render", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? UnplayedCount { get; set; }
-
- public int? Blur { get; set; }
-
- [ApiMember(Name = "BackgroundColor", Description = "Optional. Apply a background color for transparent images.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string BackgroundColor { get; set; }
-
- [ApiMember(Name = "ForegroundLayer", Description = "Optional. Apply a foreground layer on top of the image.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ForegroundLayer { get; set; }
-
- public ImageRequest()
- {
- EnableImageEnhancers = true;
- }
- }
-
- /// <summary>
- /// Class DeleteImageRequest.
- /// </summary>
- public class DeleteImageRequest
- {
- /// <summary>
- /// Gets or sets the type of the image.
- /// </summary>
- /// <value>The type of the image.</value>
- [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET,POST,DELETE")]
- public ImageType Type { get; set; }
-
- /// <summary>
- /// Gets or sets the index.
- /// </summary>
- /// <value>The index.</value>
- [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET,POST,DELETE")]
- public int? Index { get; set; }
- }
-}
diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
deleted file mode 100644
index 8426a9a4f..000000000
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ /dev/null
@@ -1,906 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-using User = Jellyfin.Data.Entities.User;
-
-namespace MediaBrowser.Api.Images
-{
- /// <summary>
- /// Class GetItemImage.
- /// </summary>
- [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")]
- [Authenticated]
- public class GetItemImageInfos : IReturn<List<ImageInfo>>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Items/{Id}/Images/{Type}", "GET")]
- [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
- [Route("/Items/{Id}/Images/{Type}", "HEAD")]
- [Route("/Items/{Id}/Images/{Type}/{Index}", "HEAD")]
- [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "GET")]
- [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "HEAD")]
- public class GetItemImage : ImageRequest
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class UpdateItemImageIndex.
- /// </summary>
- [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST", Summary = "Updates the index for an item image")]
- [Authenticated(Roles = "admin")]
- public class UpdateItemImageIndex : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the type of the image.
- /// </summary>
- /// <value>The type of the image.</value>
- [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public ImageType Type { get; set; }
-
- /// <summary>
- /// Gets or sets the index.
- /// </summary>
- /// <value>The index.</value>
- [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
- public int Index { get; set; }
-
- /// <summary>
- /// Gets or sets the new index.
- /// </summary>
- /// <value>The new index.</value>
- [ApiMember(Name = "NewIndex", Description = "The new image index", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public int NewIndex { get; set; }
- }
-
- /// <summary>
- /// Class GetPersonImage.
- /// </summary>
- [Route("/Artists/{Name}/Images/{Type}", "GET")]
- [Route("/Artists/{Name}/Images/{Type}/{Index}", "GET")]
- [Route("/Genres/{Name}/Images/{Type}", "GET")]
- [Route("/Genres/{Name}/Images/{Type}/{Index}", "GET")]
- [Route("/MusicGenres/{Name}/Images/{Type}", "GET")]
- [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "GET")]
- [Route("/Persons/{Name}/Images/{Type}", "GET")]
- [Route("/Persons/{Name}/Images/{Type}/{Index}", "GET")]
- [Route("/Studios/{Name}/Images/{Type}", "GET")]
- [Route("/Studios/{Name}/Images/{Type}/{Index}", "GET")]
- ////[Route("/Years/{Year}/Images/{Type}", "GET")]
- ////[Route("/Years/{Year}/Images/{Type}/{Index}", "GET")]
- [Route("/Artists/{Name}/Images/{Type}", "HEAD")]
- [Route("/Artists/{Name}/Images/{Type}/{Index}", "HEAD")]
- [Route("/Genres/{Name}/Images/{Type}", "HEAD")]
- [Route("/Genres/{Name}/Images/{Type}/{Index}", "HEAD")]
- [Route("/MusicGenres/{Name}/Images/{Type}", "HEAD")]
- [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "HEAD")]
- [Route("/Persons/{Name}/Images/{Type}", "HEAD")]
- [Route("/Persons/{Name}/Images/{Type}/{Index}", "HEAD")]
- [Route("/Studios/{Name}/Images/{Type}", "HEAD")]
- [Route("/Studios/{Name}/Images/{Type}/{Index}", "HEAD")]
- ////[Route("/Years/{Year}/Images/{Type}", "HEAD")]
- ////[Route("/Years/{Year}/Images/{Type}/{Index}", "HEAD")]
- public class GetItemByNameImage : ImageRequest
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "Item name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
- }
-
- /// <summary>
- /// Class GetUserImage.
- /// </summary>
- [Route("/Users/{Id}/Images/{Type}", "GET")]
- [Route("/Users/{Id}/Images/{Type}/{Index}", "GET")]
- [Route("/Users/{Id}/Images/{Type}", "HEAD")]
- [Route("/Users/{Id}/Images/{Type}/{Index}", "HEAD")]
- public class GetUserImage : ImageRequest
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class DeleteItemImage.
- /// </summary>
- [Route("/Items/{Id}/Images/{Type}", "DELETE")]
- [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")]
- [Authenticated(Roles = "admin")]
- public class DeleteItemImage : DeleteImageRequest, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class DeleteUserImage.
- /// </summary>
- [Route("/Users/{Id}/Images/{Type}", "DELETE")]
- [Route("/Users/{Id}/Images/{Type}/{Index}", "DELETE")]
- [Authenticated]
- public class DeleteUserImage : DeleteImageRequest, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class PostUserImage.
- /// </summary>
- [Route("/Users/{Id}/Images/{Type}", "POST")]
- [Route("/Users/{Id}/Images/{Type}/{Index}", "POST")]
- [Authenticated]
- public class PostUserImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// The raw Http Request Input Stream.
- /// </summary>
- /// <value>The request stream.</value>
- public Stream RequestStream { get; set; }
- }
-
- /// <summary>
- /// Class PostItemImage.
- /// </summary>
- [Route("/Items/{Id}/Images/{Type}", "POST")]
- [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")]
- [Authenticated(Roles = "admin")]
- public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// The raw Http Request Input Stream.
- /// </summary>
- /// <value>The request stream.</value>
- public Stream RequestStream { get; set; }
- }
-
- /// <summary>
- /// Class ImageService.
- /// </summary>
- public class ImageService : BaseApiService
- {
- private readonly IUserManager _userManager;
-
- private readonly ILibraryManager _libraryManager;
-
- private readonly IProviderManager _providerManager;
-
- private readonly IImageProcessor _imageProcessor;
- private readonly IFileSystem _fileSystem;
- private readonly IAuthorizationContext _authContext;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ImageService" /> class.
- /// </summary>
- public ImageService(
- ILogger<ImageService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- IImageProcessor imageProcessor,
- IFileSystem fileSystem,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _imageProcessor = imageProcessor;
- _fileSystem = fileSystem;
- _authContext = authContext;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetItemImageInfos request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var result = GetItemImageInfos(item);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the item image infos.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>Task{List{ImageInfo}}.</returns>
- public List<ImageInfo> GetItemImageInfos(BaseItem item)
- {
- var list = new List<ImageInfo>();
- var itemImages = item.ImageInfos;
-
- if (itemImages.Length == 0)
- {
- // short-circuit
- return list;
- }
-
- _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct
-
- foreach (var image in itemImages)
- {
- if (!item.AllowsMultipleImages(image.Type))
- {
- var info = GetImageInfo(item, image, null);
-
- if (info != null)
- {
- list.Add(info);
- }
- }
- }
-
- foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
- {
- var index = 0;
-
- // Prevent implicitly captured closure
- var currentImageType = imageType;
-
- foreach (var image in itemImages.Where(i => i.Type == currentImageType))
- {
- var info = GetImageInfo(item, image, index);
-
- if (info != null)
- {
- list.Add(info);
- }
-
- index++;
- }
- }
-
- return list;
- }
-
- private ImageInfo GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
- {
- int? width = null;
- int? height = null;
- string blurhash = null;
- long length = 0;
-
- try
- {
- if (info.IsLocalFile)
- {
- var fileInfo = _fileSystem.GetFileInfo(info.Path);
- length = fileInfo.Length;
-
- blurhash = info.BlurHash;
- width = info.Width;
- height = info.Height;
-
- if (width <= 0 || height <= 0)
- {
- width = null;
- height = null;
- }
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error getting image information for {Item}", item.Name);
- }
-
- try
- {
- return new ImageInfo
- {
- Path = info.Path,
- ImageIndex = imageIndex,
- ImageType = info.Type,
- ImageTag = _imageProcessor.GetImageCacheTag(item, info),
- Size = length,
- BlurHash = blurhash,
- Width = width,
- Height = height
- };
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error getting image information for {Path}", info.Path);
-
- return null;
- }
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetItemImage request)
- {
- return GetImage(request, request.Id, null, false);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Head(GetItemImage request)
- {
- return GetImage(request, request.Id, null, true);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetUserImage request)
- {
- var item = _userManager.GetUserById(request.Id);
-
- return GetImage(request, item, false);
- }
-
- public object Head(GetUserImage request)
- {
- var item = _userManager.GetUserById(request.Id);
-
- return GetImage(request, item, true);
- }
-
- public object Get(GetItemByNameImage request)
- {
- var type = GetPathValue(0).ToString();
-
- var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false));
-
- return GetImage(request, item.Id, item, false);
- }
-
- public object Head(GetItemByNameImage request)
- {
- var type = GetPathValue(0).ToString();
-
- var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false));
-
- return GetImage(request, item.Id, item, true);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(PostUserImage request)
- {
- var id = Guid.Parse(GetPathValue(1));
-
- AssertCanUpdateUser(_authContext, _userManager, id, true);
-
- request.Type = Enum.Parse<ImageType>(GetPathValue(3).ToString(), true);
-
- var user = _userManager.GetUserById(id);
-
- return PostImage(user, request.RequestStream, Request.ContentType);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(PostItemImage request)
- {
- var id = Guid.Parse(GetPathValue(1));
-
- request.Type = Enum.Parse<ImageType>(GetPathValue(3).ToString(), true);
-
- var item = _libraryManager.GetItemById(id);
-
- return PostImage(item, request.RequestStream, request.Type, Request.ContentType);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Delete(DeleteUserImage request)
- {
- var userId = request.Id;
- AssertCanUpdateUser(_authContext, _userManager, userId, true);
-
- var user = _userManager.GetUserById(userId);
- try
- {
- File.Delete(user.ProfileImage.Path);
- }
- catch (IOException e)
- {
- Logger.LogError(e, "Error deleting user profile image:");
- }
-
- _userManager.ClearProfileImage(user);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Delete(DeleteItemImage request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- item.DeleteImage(request.Type, request.Index ?? 0);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(UpdateItemImageIndex request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- UpdateItemIndex(item, request.Type, request.Index, request.NewIndex);
- }
-
- /// <summary>
- /// Updates the index of the item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="type">The type.</param>
- /// <param name="currentIndex">Index of the current.</param>
- /// <param name="newIndex">The new index.</param>
- /// <returns>Task.</returns>
- private void UpdateItemIndex(BaseItem item, ImageType type, int currentIndex, int newIndex)
- {
- item.SwapImages(type, currentIndex, newIndex);
- }
-
- /// <summary>
- /// Gets the image.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="item">The item.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ResourceNotFoundException"></exception>
- public Task<object> GetImage(ImageRequest request, Guid itemId, BaseItem item, bool isHeadRequest)
- {
- if (request.PercentPlayed.HasValue)
- {
- if (request.PercentPlayed.Value <= 0)
- {
- request.PercentPlayed = null;
- }
- else if (request.PercentPlayed.Value >= 100)
- {
- request.PercentPlayed = null;
- request.AddPlayedIndicator = true;
- }
- }
-
- if (request.PercentPlayed.HasValue)
- {
- request.UnplayedCount = null;
- }
-
- if (request.UnplayedCount.HasValue
- && request.UnplayedCount.Value <= 0)
- {
- request.UnplayedCount = null;
- }
-
- if (item == null)
- {
- item = _libraryManager.GetItemById(itemId);
-
- if (item == null)
- {
- throw new ResourceNotFoundException(string.Format("Item {0} not found.", itemId.ToString("N", CultureInfo.InvariantCulture)));
- }
- }
-
- var imageInfo = GetImageInfo(request, item);
- if (imageInfo == null)
- {
- throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
- }
-
- bool cropWhitespace;
- if (request.CropWhitespace.HasValue)
- {
- cropWhitespace = request.CropWhitespace.Value;
- }
- else
- {
- cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
- }
-
- var outputFormats = GetOutputFormats(request);
-
- TimeSpan? cacheDuration = null;
-
- if (!string.IsNullOrEmpty(request.Tag))
- {
- cacheDuration = TimeSpan.FromDays(365);
- }
-
- var responseHeaders = new Dictionary<string, string>
- {
- {"transferMode.dlna.org", "Interactive"},
- {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
- };
-
- return GetImageResult(
- item,
- itemId,
- request,
- imageInfo,
- cropWhitespace,
- outputFormats,
- cacheDuration,
- responseHeaders,
- isHeadRequest);
- }
-
- public Task<object> GetImage(ImageRequest request, User user, bool isHeadRequest)
- {
- var imageInfo = GetImageInfo(request, user);
-
- TimeSpan? cacheDuration = null;
-
- if (!string.IsNullOrEmpty(request.Tag))
- {
- cacheDuration = TimeSpan.FromDays(365);
- }
-
- var responseHeaders = new Dictionary<string, string>
- {
- {"transferMode.dlna.org", "Interactive"},
- {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
- };
-
- var outputFormats = GetOutputFormats(request);
-
- return GetImageResult(user.Id,
- request,
- imageInfo,
- outputFormats,
- cacheDuration,
- responseHeaders,
- isHeadRequest);
- }
-
- private async Task<object> GetImageResult(
- Guid itemId,
- ImageRequest request,
- ItemImageInfo info,
- IReadOnlyCollection<ImageFormat> supportedFormats,
- TimeSpan? cacheDuration,
- IDictionary<string, string> headers,
- bool isHeadRequest)
- {
- info.Type = ImageType.Profile;
- var options = new ImageProcessingOptions
- {
- CropWhiteSpace = true,
- Height = request.Height,
- ImageIndex = request.Index ?? 0,
- Image = info,
- Item = null, // Hack alert
- ItemId = itemId,
- MaxHeight = request.MaxHeight,
- MaxWidth = request.MaxWidth,
- Quality = request.Quality ?? 100,
- Width = request.Width,
- AddPlayedIndicator = request.AddPlayedIndicator,
- PercentPlayed = 0,
- UnplayedCount = request.UnplayedCount,
- Blur = request.Blur,
- BackgroundColor = request.BackgroundColor,
- ForegroundLayer = request.ForegroundLayer,
- SupportedOutputFormats = supportedFormats
- };
-
- var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
-
- headers[HeaderNames.Vary] = HeaderNames.Accept;
-
- return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- CacheDuration = cacheDuration,
- ResponseHeaders = headers,
- ContentType = imageResult.Item2,
- DateLastModified = imageResult.Item3,
- IsHeadRequest = isHeadRequest,
- Path = imageResult.Item1,
-
- FileShare = FileShare.Read
-
- }).ConfigureAwait(false);
- }
-
- private async Task<object> GetImageResult(
- BaseItem item,
- Guid itemId,
- ImageRequest request,
- ItemImageInfo image,
- bool cropwhitespace,
- IReadOnlyCollection<ImageFormat> supportedFormats,
- TimeSpan? cacheDuration,
- IDictionary<string, string> headers,
- bool isHeadRequest)
- {
- if (!image.IsLocalFile)
- {
- item ??= _libraryManager.GetItemById(itemId);
- image = await _libraryManager.ConvertImageToLocal(item, image, request.Index ?? 0).ConfigureAwait(false);
- }
-
- var options = new ImageProcessingOptions
- {
- CropWhiteSpace = cropwhitespace,
- Height = request.Height,
- ImageIndex = request.Index ?? 0,
- Image = image,
- Item = item,
- ItemId = itemId,
- MaxHeight = request.MaxHeight,
- MaxWidth = request.MaxWidth,
- Quality = request.Quality ?? 100,
- Width = request.Width,
- AddPlayedIndicator = request.AddPlayedIndicator,
- PercentPlayed = request.PercentPlayed ?? 0,
- UnplayedCount = request.UnplayedCount,
- Blur = request.Blur,
- BackgroundColor = request.BackgroundColor,
- ForegroundLayer = request.ForegroundLayer,
- SupportedOutputFormats = supportedFormats
- };
-
- var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
-
- headers[HeaderNames.Vary] = HeaderNames.Accept;
-
- return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- CacheDuration = cacheDuration,
- ResponseHeaders = headers,
- ContentType = imageResult.Item2,
- DateLastModified = imageResult.Item3,
- IsHeadRequest = isHeadRequest,
- Path = imageResult.Item1,
-
- FileShare = FileShare.Read
- }).ConfigureAwait(false);
- }
-
- private ImageFormat[] GetOutputFormats(ImageRequest request)
- {
- if (!string.IsNullOrWhiteSpace(request.Format)
- && Enum.TryParse(request.Format, true, out ImageFormat format))
- {
- return new[] { format };
- }
-
- return GetClientSupportedFormats();
- }
-
- private ImageFormat[] GetClientSupportedFormats()
- {
- var supportedFormats = Request.AcceptTypes ?? Array.Empty<string>();
- if (supportedFormats.Length > 0)
- {
- for (int i = 0; i < supportedFormats.Length; i++)
- {
- int index = supportedFormats[i].IndexOf(';');
- if (index != -1)
- {
- supportedFormats[i] = supportedFormats[i].Substring(0, index);
- }
- }
- }
-
- var acceptParam = Request.QueryString["accept"];
-
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
-
- if (!supportsWebP)
- {
- var userAgent = Request.UserAgent ?? string.Empty;
- if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 &&
- userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1)
- {
- supportsWebP = true;
- }
- }
-
- var formats = new List<ImageFormat>(4);
-
- if (supportsWebP)
- {
- formats.Add(ImageFormat.Webp);
- }
-
- formats.Add(ImageFormat.Jpg);
- formats.Add(ImageFormat.Png);
-
- if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
- {
- formats.Add(ImageFormat.Gif);
- }
-
- return formats.ToArray();
- }
-
- private bool SupportsFormat(IEnumerable<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
- {
- var mimeType = "image/" + format;
-
- if (requestAcceptTypes.Contains(mimeType))
- {
- return true;
- }
-
- if (acceptAll && requestAcceptTypes.Contains("*/*"))
- {
- return true;
- }
-
- return string.Equals(Request.QueryString["accept"], format, StringComparison.OrdinalIgnoreCase);
- }
-
- /// <summary>
- /// Gets the image path.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="item">The item.</param>
- /// <returns>System.String.</returns>
- private static ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
- {
- var index = request.Index ?? 0;
-
- return item.GetImageInfo(request.Type, index);
- }
-
- private static ItemImageInfo GetImageInfo(ImageRequest request, User user)
- {
- var info = new ItemImageInfo
- {
- Path = user.ProfileImage.Path,
- Type = ImageType.Primary,
- DateModified = user.ProfileImage.LastModified,
- };
-
- if (request.Width.HasValue)
- {
- info.Width = request.Width.Value;
- }
-
- if (request.Height.HasValue)
- {
- info.Height = request.Height.Value;
- }
-
- return info;
- }
-
- /// <summary>
- /// Posts the image.
- /// </summary>
- /// <param name="entity">The entity.</param>
- /// <param name="inputStream">The input stream.</param>
- /// <param name="imageType">Type of the image.</param>
- /// <param name="mimeType">Type of the MIME.</param>
- /// <returns>Task.</returns>
- public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
- {
- var memoryStream = await GetMemoryStream(inputStream);
-
- // Handle image/png; charset=utf-8
- mimeType = mimeType.Split(';').FirstOrDefault();
-
- await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
-
- entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
- }
-
- private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
- {
- using var reader = new StreamReader(inputStream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- var bytes = Convert.FromBase64String(text);
- return new MemoryStream(bytes)
- {
- Position = 0
- };
- }
-
- private async Task PostImage(User user, Stream inputStream, string mimeType)
- {
- var memoryStream = await GetMemoryStream(inputStream);
-
- // Handle image/png; charset=utf-8
- mimeType = mimeType.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
- user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
-
- await _providerManager
- .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
- .ConfigureAwait(false);
- await _userManager.UpdateUserAsync(user);
- }
- }
-}
diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs
deleted file mode 100644
index 86464b4b9..000000000
--- a/MediaBrowser.Api/Images/RemoteImageService.cs
+++ /dev/null
@@ -1,296 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
- public class BaseRemoteImageRequest : IReturn<RemoteImageResult>
- {
- [ApiMember(Name = "Type", Description = "The image type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public ImageType? Type { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "ProviderName", Description = "Optional. The image provider to use", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ProviderName { get; set; }
-
- [ApiMember(Name = "IncludeAllLanguages", Description = "Optional.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeAllLanguages { get; set; }
- }
-
- [Route("/Items/{Id}/RemoteImages", "GET", Summary = "Gets available remote images for an item")]
- [Authenticated]
- public class GetRemoteImages : BaseRemoteImageRequest
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Items/{Id}/RemoteImages/Providers", "GET", Summary = "Gets available remote image providers for an item")]
- [Authenticated]
- public class GetRemoteImageProviders : IReturn<List<ImageProviderInfo>>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- public class BaseDownloadRemoteImage : IReturnVoid
- {
- [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public ImageType Type { get; set; }
-
- [ApiMember(Name = "ProviderName", Description = "The image provider", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string ProviderName { get; set; }
-
- [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string ImageUrl { get; set; }
- }
-
- [Route("/Items/{Id}/RemoteImages/Download", "POST", Summary = "Downloads a remote image for an item")]
- [Authenticated(Roles = "Admin")]
- public class DownloadRemoteImage : BaseDownloadRemoteImage
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- [Route("/Images/Remote", "GET", Summary = "Gets a remote image")]
- public class GetRemoteImage
- {
- [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ImageUrl { get; set; }
- }
-
- public class RemoteImageService : BaseApiService
- {
- private readonly IProviderManager _providerManager;
-
- private readonly IServerApplicationPaths _appPaths;
- private readonly IHttpClient _httpClient;
- private readonly IFileSystem _fileSystem;
-
- private readonly ILibraryManager _libraryManager;
-
- public RemoteImageService(
- ILogger<RemoteImageService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IProviderManager providerManager,
- IServerApplicationPaths appPaths,
- IHttpClient httpClient,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _providerManager = providerManager;
- _appPaths = appPaths;
- _httpClient = httpClient;
- _fileSystem = fileSystem;
- _libraryManager = libraryManager;
- }
-
- public object Get(GetRemoteImageProviders request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var result = GetImageProviders(item);
-
- return ToOptimizedResult(result);
- }
-
- private List<ImageProviderInfo> GetImageProviders(BaseItem item)
- {
- return _providerManager.GetRemoteImageProviderInfo(item).ToList();
- }
-
- public async Task<object> Get(GetRemoteImages request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery(request.ProviderName)
- {
- IncludeAllLanguages = request.IncludeAllLanguages,
- IncludeDisabledProviders = true,
- ImageType = request.Type
- }, CancellationToken.None).ConfigureAwait(false);
-
- var imagesList = images.ToArray();
-
- var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
-
- if (request.Type.HasValue)
- {
- allProviders = allProviders.Where(i => i.SupportedImages.Contains(request.Type.Value));
- }
-
- var result = new RemoteImageResult
- {
- TotalRecordCount = imagesList.Length,
- Providers = allProviders.Select(i => i.Name)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray()
- };
-
- if (request.StartIndex.HasValue)
- {
- imagesList = imagesList.Skip(request.StartIndex.Value)
- .ToArray();
- }
-
- if (request.Limit.HasValue)
- {
- imagesList = imagesList.Take(request.Limit.Value)
- .ToArray();
- }
-
- result.Images = imagesList;
-
- return result;
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(DownloadRemoteImage request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- return DownloadRemoteImage(item, request);
- }
-
- /// <summary>
- /// Downloads the remote image.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="request">The request.</param>
- /// <returns>Task.</returns>
- private async Task DownloadRemoteImage(BaseItem item, BaseDownloadRemoteImage request)
- {
- await _providerManager.SaveImage(item, request.ImageUrl, request.Type, null, CancellationToken.None).ConfigureAwait(false);
-
- item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public async Task<object> Get(GetRemoteImage request)
- {
- var urlHash = request.ImageUrl.GetMD5();
- var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
- string contentPath;
-
- try
- {
- contentPath = File.ReadAllText(pointerCachePath);
-
- if (File.Exists(contentPath))
- {
- return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
- }
- }
- catch (FileNotFoundException)
- {
- // Means the file isn't cached yet
- }
- catch (IOException)
- {
- // Means the file isn't cached yet
- }
-
- await DownloadImage(request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
- // Read the pointer file again
- contentPath = File.ReadAllText(pointerCachePath);
-
- return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Downloads the image.
- /// </summary>
- /// <param name="url">The URL.</param>
- /// <param name="urlHash">The URL hash.</param>
- /// <param name="pointerCachePath">The pointer cache path.</param>
- /// <returns>Task.</returns>
- private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
- {
- using var result = await _httpClient.GetResponse(new HttpRequestOptions
- {
- Url = url,
- BufferContent = false
- }).ConfigureAwait(false);
- var ext = result.ContentType.Split('/')[^1];
-
- var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
- Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
- var stream = result.Content;
- await using (stream.ConfigureAwait(false))
- {
- var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
- await using (filestream.ConfigureAwait(false))
- {
- await stream.CopyToAsync(filestream).ConfigureAwait(false);
- }
- }
-
- Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
- File.WriteAllText(pointerCachePath, fullCachePath);
- }
-
- /// <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(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
- }
- }
-}
diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs
deleted file mode 100644
index 862411209..000000000
--- a/MediaBrowser.Api/ItemLookupService.cs
+++ /dev/null
@@ -1,336 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Items/{Id}/ExternalIdInfos", "GET", Summary = "Gets external id infos for an item")]
- [Authenticated(Roles = "Admin")]
- public class GetExternalIdInfos : IReturn<List<ExternalIdInfo>>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
- }
-
- [Route("/Items/RemoteSearch/Movie", "POST")]
- [Authenticated]
- public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/Trailer", "POST")]
- [Authenticated]
- public class GetTrailerRemoteSearchResults : RemoteSearchQuery<TrailerInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/MusicVideo", "POST")]
- [Authenticated]
- public class GetMusicVideoRemoteSearchResults : RemoteSearchQuery<MusicVideoInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/Series", "POST")]
- [Authenticated]
- public class GetSeriesRemoteSearchResults : RemoteSearchQuery<SeriesInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/BoxSet", "POST")]
- [Authenticated]
- public class GetBoxSetRemoteSearchResults : RemoteSearchQuery<BoxSetInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/MusicArtist", "POST")]
- [Authenticated]
- public class GetMusicArtistRemoteSearchResults : RemoteSearchQuery<ArtistInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/MusicAlbum", "POST")]
- [Authenticated]
- public class GetMusicAlbumRemoteSearchResults : RemoteSearchQuery<AlbumInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/Person", "POST")]
- [Authenticated(Roles = "Admin")]
- public class GetPersonRemoteSearchResults : RemoteSearchQuery<PersonLookupInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/Book", "POST")]
- [Authenticated]
- public class GetBookRemoteSearchResults : RemoteSearchQuery<BookInfo>, IReturn<List<RemoteSearchResult>>
- {
- }
-
- [Route("/Items/RemoteSearch/Image", "GET", Summary = "Gets a remote image")]
- public class GetRemoteSearchImage
- {
- [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ImageUrl { get; set; }
-
- [ApiMember(Name = "ProviderName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ProviderName { get; set; }
- }
-
- [Route("/Items/RemoteSearch/Apply/{Id}", "POST", Summary = "Applies search criteria to an item and refreshes metadata")]
- [Authenticated(Roles = "Admin")]
- public class ApplySearchCriteria : RemoteSearchResult, IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "The item id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Id { get; set; }
-
- [ApiMember(Name = "ReplaceAllImages", Description = "Whether or not to replace all images", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public bool ReplaceAllImages { get; set; }
-
- public ApplySearchCriteria()
- {
- ReplaceAllImages = true;
- }
- }
-
- public class ItemLookupService : BaseApiService
- {
- private readonly IProviderManager _providerManager;
- private readonly IServerApplicationPaths _appPaths;
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryManager _libraryManager;
- private readonly IJsonSerializer _json;
-
- public ItemLookupService(
- ILogger<ItemLookupService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager,
- IJsonSerializer json)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _providerManager = providerManager;
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _fileSystem = fileSystem;
- _libraryManager = libraryManager;
- _json = json;
- }
-
- public object Get(GetExternalIdInfos request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var infos = _providerManager.GetExternalIdInfos(item).ToList();
-
- return ToOptimizedResult(infos);
- }
-
- public async Task<object> Post(GetTrailerRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetBookRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetMovieRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetSeriesRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetBoxSetRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetMusicVideoRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetPersonRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetMusicAlbumRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(GetMusicArtistRemoteSearchResults request)
- {
- var result = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public Task<object> Get(GetRemoteSearchImage request)
- {
- return GetRemoteImage(request);
- }
-
- public Task Post(ApplySearchCriteria request)
- {
- var item = _libraryManager.GetItemById(new Guid(request.Id));
-
- // foreach (var key in request.ProviderIds)
- //{
- // var value = key.Value;
-
- // if (!string.IsNullOrWhiteSpace(value))
- // {
- // item.SetProviderId(key.Key, value);
- // }
- //}
- Logger.LogInformation("Setting provider id's to item {0}-{1}: {2}", item.Id, item.Name, _json.SerializeToString(request.ProviderIds));
-
- // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
- item.ProviderIds = request.ProviderIds;
- // item.ProductionYear = request.ProductionYear;
- // item.Name = request.Name;
-
- return _providerManager.RefreshFullItem(
- item,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ReplaceAllMetadata = true,
- ReplaceAllImages = request.ReplaceAllImages,
- SearchResult = request
- },
- CancellationToken.None);
- }
-
- /// <summary>
- /// Gets the remote image.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{System.Object}.</returns>
- private async Task<object> GetRemoteImage(GetRemoteSearchImage request)
- {
- var urlHash = request.ImageUrl.GetMD5();
- var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
- string contentPath;
-
- try
- {
- contentPath = File.ReadAllText(pointerCachePath);
-
- if (File.Exists(contentPath))
- {
- return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
- }
- }
- catch (FileNotFoundException)
- {
- // Means the file isn't cached yet
- }
- catch (IOException)
- {
- // Means the file isn't cached yet
- }
-
- await DownloadImage(request.ProviderName, request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
- // Read the pointer file again
- contentPath = File.ReadAllText(pointerCachePath);
-
- return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Downloads the image.
- /// </summary>
- /// <param name="providerName">Name of the provider.</param>
- /// <param name="url">The URL.</param>
- /// <param name="urlHash">The URL hash.</param>
- /// <param name="pointerCachePath">The pointer cache path.</param>
- /// <returns>Task.</returns>
- private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
- {
- var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
-
- var ext = result.ContentType.Split('/')[^1];
-
- var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
- Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
- var stream = result.Content;
-
- await using (stream.ConfigureAwait(false))
- {
- var fileStream = new FileStream(
- fullCachePath,
- FileMode.Create,
- FileAccess.Write,
- FileShare.Read,
- IODefaults.FileStreamBufferSize,
- true);
- await using (fileStream.ConfigureAwait(false))
- {
- await stream.CopyToAsync(fileStream).ConfigureAwait(false);
- }
- }
-
- Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
- File.WriteAllText(pointerCachePath, fullCachePath);
- }
-
- /// <summary>
- /// Gets the full cache path.
- /// </summary>
- /// <param name="filename">The filename.</param>
- /// <returns>System.String.</returns>
- private string GetFullCachePath(string filename)
- => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
- }
-}
diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs
deleted file mode 100644
index 5e86f04a8..000000000
--- a/MediaBrowser.Api/ItemRefreshService.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- public class BaseRefreshRequest : IReturnVoid
- {
- [ApiMember(Name = "MetadataRefreshMode", Description = "Specifies the metadata refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public MetadataRefreshMode MetadataRefreshMode { get; set; }
-
- [ApiMember(Name = "ImageRefreshMode", Description = "Specifies the image refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public MetadataRefreshMode ImageRefreshMode { get; set; }
-
- [ApiMember(Name = "ReplaceAllMetadata", Description = "Determines if metadata should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public bool ReplaceAllMetadata { get; set; }
-
- [ApiMember(Name = "ReplaceAllImages", Description = "Determines if images should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public bool ReplaceAllImages { get; set; }
- }
-
- [Route("/Items/{Id}/Refresh", "POST", Summary = "Refreshes metadata for an item")]
- public class RefreshItem : BaseRefreshRequest
- {
- [ApiMember(Name = "Recursive", Description = "Indicates if the refresh should occur recursively.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
- public bool Recursive { get; set; }
-
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- [Authenticated]
- public class ItemRefreshService : BaseApiService
- {
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
-
- public ItemRefreshService(
- ILogger<ItemRefreshService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- IFileSystem fileSystem)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(RefreshItem request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var options = GetRefreshOptions(request);
-
- _providerManager.QueueRefresh(item.Id, options, RefreshPriority.High);
- }
-
- private MetadataRefreshOptions GetRefreshOptions(RefreshItem request)
- {
- return new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = request.MetadataRefreshMode,
- ImageRefreshMode = request.ImageRefreshMode,
- ReplaceAllImages = request.ReplaceAllImages,
- ReplaceAllMetadata = request.ReplaceAllMetadata,
- ForceSave = request.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || request.ImageRefreshMode == MetadataRefreshMode.FullRefresh || request.ReplaceAllImages || request.ReplaceAllMetadata,
- IsAutomated = false
- };
- }
- }
-}
diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs
deleted file mode 100644
index 6555864dc..000000000
--- a/MediaBrowser.Api/Library/LibraryService.cs
+++ /dev/null
@@ -1,1124 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Api.Movies;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-using Book = MediaBrowser.Controller.Entities.Book;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
-using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
-
-namespace MediaBrowser.Api.Library
-{
- [Route("/Items/{Id}/File", "GET", Summary = "Gets the original file of an item")]
- [Authenticated]
- public class GetFile
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class GetCriticReviews.
- /// </summary>
- [Route("/Items/{Id}/CriticReviews", "GET", Summary = "Gets critic reviews for an item")]
- [Authenticated]
- public class GetCriticReviews : IReturn<QueryResult<BaseItemDto>>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
- }
-
- /// <summary>
- /// Class GetThemeSongs.
- /// </summary>
- [Route("/Items/{Id}/ThemeSongs", "GET", Summary = "Gets theme songs for an item")]
- [Authenticated]
- public class GetThemeSongs : IReturn<ThemeMediaResult>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool InheritFromParent { get; set; }
- }
-
- /// <summary>
- /// Class GetThemeVideos.
- /// </summary>
- [Route("/Items/{Id}/ThemeVideos", "GET", Summary = "Gets theme videos for an item")]
- [Authenticated]
- public class GetThemeVideos : IReturn<ThemeMediaResult>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool InheritFromParent { get; set; }
- }
-
- /// <summary>
- /// Class GetThemeVideos.
- /// </summary>
- [Route("/Items/{Id}/ThemeMedia", "GET", Summary = "Gets theme videos and songs for an item")]
- [Authenticated]
- public class GetThemeMedia : IReturn<AllThemeMediaResult>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool InheritFromParent { get; set; }
- }
-
- [Route("/Library/Refresh", "POST", Summary = "Starts a library scan")]
- [Authenticated(Roles = "Admin")]
- public class RefreshLibrary : IReturnVoid
- {
- }
-
- [Route("/Items/{Id}", "DELETE", Summary = "Deletes an item from the library and file system")]
- [Authenticated]
- public class DeleteItem : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/Items", "DELETE", Summary = "Deletes an item from the library and file system")]
- [Authenticated]
- public class DeleteItems : IReturnVoid
- {
- [ApiMember(Name = "Ids", Description = "Ids", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string Ids { get; set; }
- }
-
- [Route("/Items/Counts", "GET")]
- [Authenticated]
- public class GetItemCounts : IReturn<ItemCounts>
- {
- [ApiMember(Name = "UserId", Description = "Optional. Get counts from a specific user's library.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "IsFavorite", Description = "Optional. Get counts of favorite items", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsFavorite { get; set; }
- }
-
- [Route("/Items/{Id}/Ancestors", "GET", Summary = "Gets all parents of an item")]
- [Authenticated]
- public class GetAncestors : IReturn<BaseItemDto[]>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class GetPhyscialPaths.
- /// </summary>
- [Route("/Library/PhysicalPaths", "GET", Summary = "Gets a list of physical paths from virtual folders")]
- [Authenticated(Roles = "Admin")]
- public class GetPhyscialPaths : IReturn<List<string>>
- {
- }
-
- [Route("/Library/MediaFolders", "GET", Summary = "Gets all user media folders.")]
- [Authenticated]
- public class GetMediaFolders : IReturn<QueryResult<BaseItemDto>>
- {
- [ApiMember(Name = "IsHidden", Description = "Optional. Filter by folders that are marked hidden, or not.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? IsHidden { get; set; }
- }
-
- [Route("/Library/Series/Added", "POST", Summary = "Reports that new episodes of a series have been added by an external source")]
- [Route("/Library/Series/Updated", "POST", Summary = "Reports that new episodes of a series have been added by an external source")]
- [Authenticated]
- public class PostUpdatedSeries : IReturnVoid
- {
- [ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string TvdbId { get; set; }
- }
-
- [Route("/Library/Movies/Added", "POST", Summary = "Reports that new movies have been added by an external source")]
- [Route("/Library/Movies/Updated", "POST", Summary = "Reports that new movies have been added by an external source")]
- [Authenticated]
- public class PostUpdatedMovies : IReturnVoid
- {
- [ApiMember(Name = "TmdbId", Description = "Tmdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string TmdbId { get; set; }
- [ApiMember(Name = "ImdbId", Description = "Imdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string ImdbId { get; set; }
- }
-
- public class MediaUpdateInfo
- {
- public string Path { get; set; }
-
- // Created, Modified, Deleted
- public string UpdateType { get; set; }
- }
-
- [Route("/Library/Media/Updated", "POST", Summary = "Reports that new movies have been added by an external source")]
- [Authenticated]
- public class PostUpdatedMedia : IReturnVoid
- {
- [ApiMember(Name = "Updates", Description = "A list of updated media paths", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
- public List<MediaUpdateInfo> Updates { get; set; }
- }
-
- [Route("/Items/{Id}/Download", "GET", Summary = "Downloads item media")]
- [Authenticated(Roles = "download")]
- public class GetDownload
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Artists/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
- [Route("/Items/{Id}/Similar", "GET", Summary = "Gets similar items")]
- [Route("/Albums/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
- [Route("/Shows/{Id}/Similar", "GET", Summary = "Finds tv shows similar to a given one.")]
- [Route("/Movies/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given movie.")]
- [Route("/Trailers/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given trailer.")]
- [Authenticated]
- public class GetSimilarItems : BaseGetSimilarItemsFromItem
- {
- }
-
- [Route("/Libraries/AvailableOptions", "GET")]
- [Authenticated(AllowBeforeStartupWizard = true)]
- public class GetLibraryOptionsInfo : IReturn<LibraryOptionsResult>
- {
- public string LibraryContentType { get; set; }
-
- public bool IsNewLibrary { get; set; }
- }
-
- public class LibraryOptionInfo
- {
- public string Name { get; set; }
-
- public bool DefaultEnabled { get; set; }
- }
-
- public class LibraryOptionsResult
- {
- public LibraryOptionInfo[] MetadataSavers { get; set; }
-
- public LibraryOptionInfo[] MetadataReaders { get; set; }
-
- public LibraryOptionInfo[] SubtitleFetchers { get; set; }
-
- public LibraryTypeOptions[] TypeOptions { get; set; }
- }
-
- public class LibraryTypeOptions
- {
- public string Type { get; set; }
-
- public LibraryOptionInfo[] MetadataFetchers { get; set; }
-
- public LibraryOptionInfo[] ImageFetchers { get; set; }
-
- public ImageType[] SupportedImageTypes { get; set; }
-
- public ImageOption[] DefaultImageOptions { get; set; }
- }
-
- /// <summary>
- /// Class LibraryService.
- /// </summary>
- public class LibraryService : BaseApiService
- {
- private readonly IProviderManager _providerManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
- private readonly IActivityManager _activityManager;
- private readonly ILocalizationManager _localization;
- private readonly ILibraryMonitor _libraryMonitor;
-
- private readonly ILogger<MoviesService> _moviesServiceLogger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryService" /> class.
- /// </summary>
- public LibraryService(
- ILogger<LibraryService> logger,
- ILogger<MoviesService> moviesServiceLogger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IProviderManager providerManager,
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService,
- IAuthorizationContext authContext,
- IActivityManager activityManager,
- ILocalizationManager localization,
- ILibraryMonitor libraryMonitor)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _providerManager = providerManager;
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- _authContext = authContext;
- _activityManager = activityManager;
- _localization = localization;
- _libraryMonitor = libraryMonitor;
- _moviesServiceLogger = moviesServiceLogger;
- }
-
- // Content Types available for each Library
- private string[] GetRepresentativeItemTypes(string contentType)
- {
- return contentType switch
- {
- CollectionType.BoxSets => new[] {"BoxSet"},
- CollectionType.Playlists => new[] {"Playlist"},
- CollectionType.Movies => new[] {"Movie"},
- CollectionType.TvShows => new[] {"Series", "Season", "Episode"},
- CollectionType.Books => new[] {"Book"},
- CollectionType.Music => new[] {"MusicArtist", "MusicAlbum", "Audio", "MusicVideo"},
- CollectionType.HomeVideos => new[] {"Video", "Photo"},
- CollectionType.Photos => new[] {"Video", "Photo"},
- CollectionType.MusicVideos => new[] {"MusicVideo"},
- _ => new[] {"Series", "Season", "Episode", "Movie"}
- };
- }
-
- private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
- {
- if (isNewLibrary)
- {
- return false;
- }
-
- var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
- .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- .ToArray();
-
- if (metadataOptions.Length == 0)
- {
- return true;
- }
-
- return metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase));
- }
-
- private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
- {
- if (isNewLibrary)
- {
- if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
- {
- return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
- || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
- || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
- }
-
- return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
- }
-
- var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .ToArray();
-
- return metadataOptions.Length == 0
- || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
- }
-
- private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
- {
- if (isNewLibrary)
- {
- if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
- {
- return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
- }
-
- return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
- }
-
- var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .ToArray();
-
- if (metadataOptions.Length == 0)
- {
- return true;
- }
-
- return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
- }
-
- public object Get(GetLibraryOptionsInfo request)
- {
- var result = new LibraryOptionsResult();
-
- var types = GetRepresentativeItemTypes(request.LibraryContentType);
- var isNewLibrary = request.IsNewLibrary;
- var typesList = types.ToList();
-
- var plugins = _providerManager.GetAllMetadataPlugins()
- .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase))
- .OrderBy(i => typesList.IndexOf(i.ItemType))
- .ToList();
-
- result.MetadataSavers = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
- .Select(i => new LibraryOptionInfo
- {
- Name = i.Name,
- DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
- })
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
- .ToArray();
-
- result.MetadataReaders = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
- .Select(i => new LibraryOptionInfo
- {
- Name = i.Name,
- DefaultEnabled = true
- })
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
- .ToArray();
-
- result.SubtitleFetchers = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
- .Select(i => new LibraryOptionInfo
- {
- Name = i.Name,
- DefaultEnabled = true
- })
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
- .ToArray();
-
- var typeOptions = new List<LibraryTypeOptions>();
-
- foreach (var type in types)
- {
- TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
-
- typeOptions.Add(new LibraryTypeOptions
- {
- Type = type,
-
- MetadataFetchers = plugins
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
- .Select(i => new LibraryOptionInfo
- {
- Name = i.Name,
- DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
- })
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
- .ToArray(),
-
- ImageFetchers = plugins
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
- .Select(i => new LibraryOptionInfo
- {
- Name = i.Name,
- DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
- })
- .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First())
- .ToArray(),
-
- SupportedImageTypes = plugins
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
- .Distinct()
- .ToArray(),
-
- DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
- });
- }
-
- result.TypeOptions = typeOptions.ToArray();
-
- return result;
- }
-
- public object Get(GetSimilarItems request)
- {
- var item = string.IsNullOrEmpty(request.Id) ?
- (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
- _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
- var program = item as IHasProgramAttributes;
-
- if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
- {
- return new MoviesService(
- _moviesServiceLogger,
- ServerConfigurationManager,
- ResultFactory,
- _userManager,
- _libraryManager,
- _dtoService,
- _authContext)
- {
- Request = Request,
- }.GetSimilarItemsResult(request);
- }
-
- if (program != null && program.IsSeries)
- {
- return GetSimilarItemsResult(request, new[] { typeof(Series).Name });
- }
-
- if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
- {
- return new QueryResult<BaseItemDto>();
- }
-
- return GetSimilarItemsResult(request, new[] { item.GetType().Name });
- }
-
- private QueryResult<BaseItemDto> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, string[] includeItemTypes)
- {
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var item = string.IsNullOrEmpty(request.Id) ?
- (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
- _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var query = new InternalItemsQuery(user)
- {
- Limit = request.Limit,
- IncludeItemTypes = includeItemTypes,
- SimilarTo = item,
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = false
- };
-
- // ExcludeArtistIds
- if (!string.IsNullOrEmpty(request.ExcludeArtistIds))
- {
- query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds);
- }
-
- List<BaseItem> itemsResult;
-
- if (item is MusicArtist)
- {
- query.IncludeItemTypes = Array.Empty<string>();
-
- itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
- }
- else
- {
- itemsResult = _libraryManager.GetItemList(query);
- }
-
- var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = returnList,
- TotalRecordCount = itemsResult.Count
- };
-
- return result;
- }
-
- public object Get(GetMediaFolders request)
- {
- var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
-
- if (request.IsHidden.HasValue)
- {
- var val = request.IsHidden.Value;
-
- items = items.Where(i => i.IsHidden == val).ToList();
- }
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = new QueryResult<BaseItemDto>
- {
- TotalRecordCount = items.Count,
-
- Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray()
- };
-
- return result;
- }
-
- public void Post(PostUpdatedSeries request)
- {
- var series = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { typeof(Series).Name },
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- }).Where(i => string.Equals(request.TvdbId, i.GetProviderId(MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
-
- foreach (var item in series)
- {
- _libraryMonitor.ReportFileSystemChanged(item.Path);
- }
- }
-
- public void Post(PostUpdatedMedia request)
- {
- if (request.Updates != null)
- {
- foreach (var item in request.Updates)
- {
- _libraryMonitor.ReportFileSystemChanged(item.Path);
- }
- }
- }
-
- public void Post(PostUpdatedMovies request)
- {
- var movies = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { typeof(Movie).Name },
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- });
-
- if (!string.IsNullOrWhiteSpace(request.ImdbId))
- {
- movies = movies.Where(i => string.Equals(request.ImdbId, i.GetProviderId(MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
- }
- else if (!string.IsNullOrWhiteSpace(request.TmdbId))
- {
- movies = movies.Where(i => string.Equals(request.TmdbId, i.GetProviderId(MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
- }
- else
- {
- movies = new List<BaseItem>();
- }
-
- foreach (var item in movies)
- {
- _libraryMonitor.ReportFileSystemChanged(item.Path);
- }
- }
-
- public Task<object> Get(GetDownload request)
- {
- var item = _libraryManager.GetItemById(request.Id);
- var auth = _authContext.GetAuthorizationInfo(Request);
-
- var user = auth.User;
-
- if (user != null)
- {
- if (!item.CanDownload(user))
- {
- throw new ArgumentException("Item does not support downloading");
- }
- }
- else
- {
- if (!item.CanDownload())
- {
- throw new ArgumentException("Item does not support downloading");
- }
- }
-
- var headers = new Dictionary<string, string>();
-
- if (user != null)
- {
- LogDownload(item, user, auth);
- }
-
- var path = item.Path;
-
- // Quotes are valid in linux. They'll possibly cause issues here
- var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
- if (!string.IsNullOrWhiteSpace(filename))
- {
- // Kestrel doesn't support non-ASCII characters in headers
- if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]"))
- {
- // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
- headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);
- }
- else
- {
- headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
- }
- }
-
- return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- Path = path,
- ResponseHeaders = headers
- });
- }
-
- private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
- {
- try
- {
- _activityManager.Create(new ActivityLog(
- string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
- "UserDownloadingContent",
- auth.UserId)
- {
- ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
- });
- }
- catch
- {
- // Logged at lower levels
- }
- }
-
- public Task<object> Get(GetFile request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- return ResultFactory.GetStaticFileResult(Request, item.Path);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetPhyscialPaths request)
- {
- var result = _libraryManager.RootFolder.Children
- .SelectMany(c => c.PhysicalLocations)
- .ToList();
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetAncestors request)
- {
- var result = GetAncestors(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the ancestors.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{BaseItemDto[]}.</returns>
- public List<BaseItemDto> GetAncestors(GetAncestors request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var baseItemDtos = new List<BaseItemDto>();
-
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- BaseItem parent = item.GetParent();
-
- while (parent != null)
- {
- if (user != null)
- {
- parent = TranslateParentItem(parent, user);
- }
-
- baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
-
- parent = parent.GetParent();
- }
-
- return baseItemDtos;
- }
-
- private BaseItem TranslateParentItem(BaseItem item, User user)
- {
- return item.GetParent() is AggregateFolder
- ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
- .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
- : item;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetCriticReviews request)
- {
- return new QueryResult<BaseItemDto>();
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetItemCounts request)
- {
- var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
- var counts = new ItemCounts
- {
- AlbumCount = GetCount(typeof(MusicAlbum), user, request),
- EpisodeCount = GetCount(typeof(Episode), user, request),
- MovieCount = GetCount(typeof(Movie), user, request),
- SeriesCount = GetCount(typeof(Series), user, request),
- SongCount = GetCount(typeof(Audio), user, request),
- MusicVideoCount = GetCount(typeof(MusicVideo), user, request),
- BoxSetCount = GetCount(typeof(BoxSet), user, request),
- BookCount = GetCount(typeof(Book), user, request)
- };
-
- return ToOptimizedResult(counts);
- }
-
- private int GetCount(Type type, User user, GetItemCounts request)
- {
- var query = new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { type.Name },
- Limit = 0,
- Recursive = true,
- IsVirtualItem = false,
- IsFavorite = request.IsFavorite,
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- };
-
- return _libraryManager.GetItemsResult(query).TotalRecordCount;
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public async Task Post(RefreshLibrary request)
- {
- try
- {
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error refreshing library");
- }
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Delete(DeleteItems request)
- {
- var ids = string.IsNullOrWhiteSpace(request.Ids)
- ? Array.Empty<string>()
- : request.Ids.Split(',');
-
- foreach (var i in ids)
- {
- var item = _libraryManager.GetItemById(i);
- var auth = _authContext.GetAuthorizationInfo(Request);
- var user = auth.User;
-
- if (!item.CanDelete(user))
- {
- if (ids.Length > 1)
- {
- throw new SecurityException("Unauthorized access");
- }
-
- continue;
- }
-
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = true
- }, true);
- }
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Delete(DeleteItem request)
- {
- Delete(new DeleteItems
- {
- Ids = request.Id
- });
- }
-
- public object Get(GetThemeMedia request)
- {
- var themeSongs = GetThemeSongs(new GetThemeSongs
- {
- InheritFromParent = request.InheritFromParent,
- Id = request.Id,
- UserId = request.UserId
-
- });
-
- var themeVideos = GetThemeVideos(new GetThemeVideos
- {
- InheritFromParent = request.InheritFromParent,
- Id = request.Id,
- UserId = request.UserId
-
- });
-
- return ToOptimizedResult(new AllThemeMediaResult
- {
- ThemeSongsResult = themeSongs,
- ThemeVideosResult = themeVideos,
-
- SoundtrackSongsResult = new ThemeMediaResult()
- });
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetThemeSongs request)
- {
- var result = GetThemeSongs(request);
-
- return ToOptimizedResult(result);
- }
-
- private ThemeMediaResult GetThemeSongs(GetThemeSongs request)
- {
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var item = string.IsNullOrEmpty(request.Id)
- ? (!request.UserId.Equals(Guid.Empty)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.RootFolder)
- : _libraryManager.GetItemById(request.Id);
-
- if (item == null)
- {
- throw new ResourceNotFoundException("Item not found.");
- }
-
- IEnumerable<BaseItem> themeItems;
-
- while (true)
- {
- themeItems = item.GetThemeSongs();
-
- if (themeItems.Any() || !request.InheritFromParent)
- {
- break;
- }
-
- var parent = item.GetParent();
- if (parent == null)
- {
- break;
- }
-
- item = parent;
- }
-
- var dtoOptions = GetDtoOptions(_authContext, request);
- var items = themeItems
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
-
- return new ThemeMediaResult
- {
- Items = items,
- TotalRecordCount = items.Length,
- OwnerId = item.Id
- };
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetThemeVideos request)
- {
- return ToOptimizedResult(GetThemeVideos(request));
- }
-
- public ThemeMediaResult GetThemeVideos(GetThemeVideos request)
- {
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var item = string.IsNullOrEmpty(request.Id)
- ? (!request.UserId.Equals(Guid.Empty)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.RootFolder)
- : _libraryManager.GetItemById(request.Id);
-
- if (item == null)
- {
- throw new ResourceNotFoundException("Item not found.");
- }
-
- IEnumerable<BaseItem> themeItems;
-
- while (true)
- {
- themeItems = item.GetThemeVideos();
-
- if (themeItems.Any() || !request.InheritFromParent)
- {
- break;
- }
-
- var parent = item.GetParent();
- if (parent == null)
- {
- break;
- }
-
- item = parent;
- }
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = themeItems
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
-
- return new ThemeMediaResult
- {
- Items = items,
- TotalRecordCount = items.Length,
- OwnerId = item.Id
- };
- }
- }
-}
diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs
deleted file mode 100644
index b69550ed1..000000000
--- a/MediaBrowser.Api/Library/LibraryStructureService.cs
+++ /dev/null
@@ -1,412 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Library
-{
- /// <summary>
- /// Class GetDefaultVirtualFolders.
- /// </summary>
- [Route("/Library/VirtualFolders", "GET")]
- public class GetVirtualFolders : IReturn<List<VirtualFolderInfo>>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- public string UserId { get; set; }
- }
-
- [Route("/Library/VirtualFolders", "POST")]
- public class AddVirtualFolder : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the type of the collection.
- /// </summary>
- /// <value>The type of the collection.</value>
- public string CollectionType { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [refresh library].
- /// </summary>
- /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
- public bool RefreshLibrary { get; set; }
-
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- public string[] Paths { get; set; }
-
- public LibraryOptions LibraryOptions { get; set; }
- }
-
- [Route("/Library/VirtualFolders", "DELETE")]
- public class RemoveVirtualFolder : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [refresh library].
- /// </summary>
- /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
- public bool RefreshLibrary { get; set; }
- }
-
- [Route("/Library/VirtualFolders/Name", "POST")]
- public class RenameVirtualFolder : IReturnVoid
- {
- /// <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 NewName { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [refresh library].
- /// </summary>
- /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
- public bool RefreshLibrary { get; set; }
- }
-
- [Route("/Library/VirtualFolders/Paths", "POST")]
- public class AddMediaPath : IReturnVoid
- {
- /// <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 Path { get; set; }
-
- public MediaPathInfo PathInfo { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [refresh library].
- /// </summary>
- /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
- public bool RefreshLibrary { get; set; }
- }
-
- [Route("/Library/VirtualFolders/Paths/Update", "POST")]
- public class UpdateMediaPath : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- public MediaPathInfo PathInfo { get; set; }
- }
-
- [Route("/Library/VirtualFolders/Paths", "DELETE")]
- public class RemoveMediaPath : IReturnVoid
- {
- /// <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 Path { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [refresh library].
- /// </summary>
- /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
- public bool RefreshLibrary { get; set; }
- }
-
- [Route("/Library/VirtualFolders/LibraryOptions", "POST")]
- public class UpdateLibraryOptions : IReturnVoid
- {
- public string Id { get; set; }
-
- public LibraryOptions LibraryOptions { get; set; }
- }
-
- /// <summary>
- /// Class LibraryStructureService.
- /// </summary>
- [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
- public class LibraryStructureService : BaseApiService
- {
- /// <summary>
- /// The _app paths.
- /// </summary>
- private readonly IServerApplicationPaths _appPaths;
-
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILibraryMonitor _libraryMonitor;
-
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryStructureService" /> class.
- /// </summary>
- public LibraryStructureService(
- ILogger<LibraryStructureService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ILibraryManager libraryManager,
- ILibraryMonitor libraryMonitor)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _libraryManager = libraryManager;
- _libraryMonitor = libraryMonitor;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetVirtualFolders request)
- {
- var result = _libraryManager.GetVirtualFolders(true);
-
- return ToOptimizedResult(result);
- }
-
- public void Post(UpdateLibraryOptions request)
- {
- var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
-
- collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(AddVirtualFolder request)
- {
- var libraryOptions = request.LibraryOptions ?? new LibraryOptions();
-
- if (request.Paths != null && request.Paths.Length > 0)
- {
- libraryOptions.PathInfos = request.Paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
- }
-
- return _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, libraryOptions, request.RefreshLibrary);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(RenameVirtualFolder request)
- {
- if (string.IsNullOrWhiteSpace(request.Name))
- {
- throw new ArgumentNullException(nameof(request));
- }
-
- if (string.IsNullOrWhiteSpace(request.NewName))
- {
- throw new ArgumentNullException(nameof(request));
- }
-
- var rootFolderPath = _appPaths.DefaultUserViewsPath;
-
- var currentPath = Path.Combine(rootFolderPath, request.Name);
- var newPath = Path.Combine(rootFolderPath, request.NewName);
-
- if (!Directory.Exists(currentPath))
- {
- throw new FileNotFoundException("The media collection does not exist");
- }
-
- if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
- {
- throw new ArgumentException("Media library already exists at " + newPath + ".");
- }
-
- _libraryMonitor.Stop();
-
- try
- {
- // Changing capitalization. Handle windows case insensitivity
- if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
- {
- var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
- Directory.Move(currentPath, tempPath);
- currentPath = tempPath;
- }
-
- Directory.Move(currentPath, newPath);
- }
- finally
- {
- CollectionFolder.OnCollectionFolderChange();
-
- Task.Run(() =>
- {
- // No need to start if scanning the library because it will handle it
- if (request.RefreshLibrary)
- {
- _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- var task = Task.Delay(1000);
- // Have to block here to allow exceptions to bubble
- Task.WaitAll(task);
-
- _libraryMonitor.Start();
- }
- });
- }
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Delete(RemoveVirtualFolder request)
- {
- return _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(AddMediaPath request)
- {
- if (string.IsNullOrWhiteSpace(request.Name))
- {
- throw new ArgumentNullException(nameof(request));
- }
-
- _libraryMonitor.Stop();
-
- try
- {
- var mediaPath = request.PathInfo ?? new MediaPathInfo
- {
- Path = request.Path
- };
-
- _libraryManager.AddMediaPath(request.Name, mediaPath);
- }
- finally
- {
- Task.Run(() =>
- {
- // No need to start if scanning the library because it will handle it
- if (request.RefreshLibrary)
- {
- _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- var task = Task.Delay(1000);
- // Have to block here to allow exceptions to bubble
- Task.WaitAll(task);
-
- _libraryMonitor.Start();
- }
- });
- }
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(UpdateMediaPath request)
- {
- if (string.IsNullOrWhiteSpace(request.Name))
- {
- throw new ArgumentNullException(nameof(request));
- }
-
- _libraryManager.UpdateMediaPath(request.Name, request.PathInfo);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Delete(RemoveMediaPath request)
- {
- if (string.IsNullOrWhiteSpace(request.Name))
- {
- throw new ArgumentNullException(nameof(request));
- }
-
- _libraryMonitor.Stop();
-
- try
- {
- _libraryManager.RemoveMediaPath(request.Name, request.Path);
- }
- finally
- {
- Task.Run(() =>
- {
- // No need to start if scanning the library because it will handle it
- if (request.RefreshLibrary)
- {
- _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- var task = Task.Delay(1000);
- // Have to block here to allow exceptions to bubble
- Task.WaitAll(task);
-
- _libraryMonitor.Start();
- }
- });
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs
deleted file mode 100644
index 830372dd8..000000000
--- a/MediaBrowser.Api/LiveTv/LiveTvService.cs
+++ /dev/null
@@ -1,1287 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-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.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace MediaBrowser.Api.LiveTv
-{
- /// <summary>
- /// This is insecure right now to avoid windows phone refactoring.
- /// </summary>
- [Route("/LiveTv/Info", "GET", Summary = "Gets available live tv services.")]
- [Authenticated]
- public class GetLiveTvInfo : IReturn<LiveTvInfo>
- {
- }
-
- [Route("/LiveTv/Channels", "GET", Summary = "Gets available live tv channels.")]
- [Authenticated]
- public class GetChannels : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- [ApiMember(Name = "Type", Description = "Optional filter by channel type.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public ChannelType? Type { get; set; }
-
- [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsMovie { get; set; }
-
- [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSeries { get; set; }
-
- [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsNews { get; set; }
-
- [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsKids { get; set; }
-
- [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSports { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "IsFavorite", Description = "Filter by channels that are favorites, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsFavorite { get; set; }
-
- [ApiMember(Name = "IsLiked", Description = "Filter by channels that are liked, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsLiked { get; set; }
-
- [ApiMember(Name = "IsDisliked", Description = "Filter by channels that are disliked, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsDisliked { get; set; }
-
- [ApiMember(Name = "EnableFavoriteSorting", Description = "Incorporate favorite and like status into channel sorting.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool EnableFavoriteSorting { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "AddCurrentProgram", Description = "Optional. Adds current program info to each channel", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool AddCurrentProgram { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- public string SortBy { get; set; }
-
- public SortOrder? SortOrder { get; set; }
-
- /// <summary>
- /// Gets the order by.
- /// </summary>
- /// <returns>IEnumerable{ItemSortBy}.</returns>
- public string[] GetOrderBy()
- {
- var val = SortBy;
-
- if (string.IsNullOrEmpty(val))
- {
- return Array.Empty<string>();
- }
-
- return val.Split(',');
- }
-
- public GetChannels()
- {
- AddCurrentProgram = true;
- }
- }
-
- [Route("/LiveTv/Channels/{Id}", "GET", Summary = "Gets a live tv channel")]
- [Authenticated]
- public class GetChannel : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- [Route("/LiveTv/Recordings", "GET", Summary = "Gets live tv recordings")]
- [Authenticated]
- public class GetRecordings : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ChannelId { get; set; }
-
- [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "Status", Description = "Optional filter by recording status.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public RecordingStatus? Status { get; set; }
-
- [ApiMember(Name = "Status", Description = "Optional filter by recordings that are in progress, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsInProgress { get; set; }
-
- [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by recordings belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SeriesTimerId { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- public bool EnableTotalRecordCount { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- public bool? IsMovie { get; set; }
-
- public bool? IsSeries { get; set; }
-
- public bool? IsKids { get; set; }
-
- public bool? IsSports { get; set; }
-
- public bool? IsNews { get; set; }
-
- public bool? IsLibraryItem { get; set; }
-
- public GetRecordings()
- {
- EnableTotalRecordCount = true;
- }
- }
-
- [Route("/LiveTv/Recordings/Series", "GET", Summary = "Gets live tv recordings")]
- [Authenticated]
- public class GetRecordingSeries : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ChannelId { get; set; }
-
- [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string UserId { get; set; }
-
- [ApiMember(Name = "GroupId", Description = "Optional filter by recording group.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string GroupId { get; set; }
-
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "Status", Description = "Optional filter by recording status.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public RecordingStatus? Status { get; set; }
-
- [ApiMember(Name = "Status", Description = "Optional filter by recordings that are in progress, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsInProgress { get; set; }
-
- [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by recordings belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SeriesTimerId { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- public bool EnableTotalRecordCount { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- public GetRecordingSeries()
- {
- EnableTotalRecordCount = true;
- }
- }
-
- [Route("/LiveTv/Recordings/Groups", "GET", Summary = "Gets live tv recording groups")]
- [Authenticated]
- public class GetRecordingGroups : IReturn<QueryResult<BaseItemDto>>
- {
- [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string UserId { get; set; }
- }
-
- [Route("/LiveTv/Recordings/Folders", "GET", Summary = "Gets recording folders")]
- [Authenticated]
- public class GetRecordingFolders : IReturn<BaseItemDto[]>
- {
- [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- [Route("/LiveTv/Recordings/{Id}", "GET", Summary = "Gets a live tv recording")]
- [Authenticated]
- public class GetRecording : IReturn<BaseItemDto>
- {
- [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- [Route("/LiveTv/Tuners/{Id}/Reset", "POST", Summary = "Resets a tv tuner")]
- [Authenticated]
- public class ResetTuner : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Tuner Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/Timers/{Id}", "GET", Summary = "Gets a live tv timer")]
- [Authenticated]
- public class GetTimer : IReturn<TimerInfoDto>
- {
- [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/Timers/Defaults", "GET", Summary = "Gets default values for a new timer")]
- [Authenticated]
- public class GetDefaultTimer : IReturn<SeriesTimerInfoDto>
- {
- [ApiMember(Name = "ProgramId", Description = "Optional, to attach default values based on a program.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ProgramId { get; set; }
- }
-
- [Route("/LiveTv/Timers", "GET", Summary = "Gets live tv timers")]
- [Authenticated]
- public class GetTimers : IReturn<QueryResult<TimerInfoDto>>
- {
- [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ChannelId { get; set; }
-
- [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by timers belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SeriesTimerId { get; set; }
-
- public bool? IsActive { get; set; }
-
- public bool? IsScheduled { get; set; }
- }
-
- [Route("/LiveTv/Programs", "GET,POST", Summary = "Gets available live tv epgs..")]
- [Authenticated]
- public class GetPrograms : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- [ApiMember(Name = "ChannelIds", Description = "The channels to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string ChannelIds { get; set; }
-
- [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "MinStartDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string MinStartDate { get; set; }
-
- [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? HasAired { get; set; }
-
- public bool? IsAiring { get; set; }
-
- [ApiMember(Name = "MaxStartDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string MaxStartDate { get; set; }
-
- [ApiMember(Name = "MinEndDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string MinEndDate { get; set; }
-
- [ApiMember(Name = "MaxEndDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string MaxEndDate { get; set; }
-
- [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsMovie { get; set; }
-
- [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSeries { get; set; }
-
- [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsNews { get; set; }
-
- [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsKids { get; set; }
-
- [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSports { get; set; }
-
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Name, StartDate", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string SortBy { get; set; }
-
- [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SortOrder { get; set; }
-
- [ApiMember(Name = "Genres", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string Genres { get; set; }
-
- [ApiMember(Name = "GenreIds", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string GenreIds { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- public bool EnableTotalRecordCount { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- public string SeriesTimerId { get; set; }
-
- public Guid LibrarySeriesId { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- public GetPrograms()
- {
- EnableTotalRecordCount = true;
- }
- }
-
- [Route("/LiveTv/Programs/Recommended", "GET", Summary = "Gets available live tv epgs..")]
- [Authenticated]
- public class GetRecommendedPrograms : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- public bool EnableTotalRecordCount { get; set; }
-
- public GetRecommendedPrograms()
- {
- EnableTotalRecordCount = true;
- }
-
- [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "IsAiring", Description = "Optional. Filter by programs that are currently airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsAiring { get; set; }
-
- [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? HasAired { get; set; }
-
- [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSeries { get; set; }
-
- [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsMovie { get; set; }
-
- [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsNews { get; set; }
-
- [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsKids { get; set; }
-
- [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSports { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- [ApiMember(Name = "GenreIds", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string GenreIds { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
- }
-
- [Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")]
- [Authenticated]
- public class GetProgram : IReturn<BaseItemDto>
- {
- [ApiMember(Name = "Id", Description = "Program Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
-
- [Route("/LiveTv/Recordings/{Id}", "DELETE", Summary = "Deletes a live tv recording")]
- [Authenticated]
- public class DeleteRecording : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid Id { get; set; }
- }
-
- [Route("/LiveTv/Timers/{Id}", "DELETE", Summary = "Cancels a live tv timer")]
- [Authenticated]
- public class CancelTimer : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/Timers/{Id}", "POST", Summary = "Updates a live tv timer")]
- [Authenticated]
- public class UpdateTimer : TimerInfoDto, IReturnVoid
- {
- }
-
- [Route("/LiveTv/Timers", "POST", Summary = "Creates a live tv timer")]
- [Authenticated]
- public class CreateTimer : TimerInfoDto, IReturnVoid
- {
- }
-
- [Route("/LiveTv/SeriesTimers/{Id}", "GET", Summary = "Gets a live tv series timer")]
- [Authenticated]
- public class GetSeriesTimer : IReturn<TimerInfoDto>
- {
- [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/SeriesTimers", "GET", Summary = "Gets live tv series timers")]
- [Authenticated]
- public class GetSeriesTimers : IReturn<QueryResult<SeriesTimerInfoDto>>
- {
- [ApiMember(Name = "SortBy", Description = "Optional. Sort by SortName or Priority", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public string SortBy { get; set; }
-
- [ApiMember(Name = "SortOrder", Description = "Optional. Sort in Ascending or Descending order", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
- public SortOrder SortOrder { get; set; }
- }
-
- [Route("/LiveTv/SeriesTimers/{Id}", "DELETE", Summary = "Cancels a live tv series timer")]
- [Authenticated]
- public class CancelSeriesTimer : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/SeriesTimers/{Id}", "POST", Summary = "Updates a live tv series timer")]
- [Authenticated]
- public class UpdateSeriesTimer : SeriesTimerInfoDto, IReturnVoid
- {
- }
-
- [Route("/LiveTv/SeriesTimers", "POST", Summary = "Creates a live tv series timer")]
- [Authenticated]
- public class CreateSeriesTimer : SeriesTimerInfoDto, IReturnVoid
- {
- }
-
- [Route("/LiveTv/Recordings/Groups/{Id}", "GET", Summary = "Gets a recording group")]
- [Authenticated]
- public class GetRecordingGroup : IReturn<BaseItemDto>
- {
- [ApiMember(Name = "Id", Description = "Recording group Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/GuideInfo", "GET", Summary = "Gets guide info")]
- [Authenticated]
- public class GetGuideInfo : IReturn<GuideInfo>
- {
- }
-
- [Route("/LiveTv/TunerHosts", "POST", Summary = "Adds a tuner host")]
- [Authenticated]
- public class AddTunerHost : TunerHostInfo, IReturn<TunerHostInfo>
- {
- }
-
- [Route("/LiveTv/TunerHosts", "DELETE", Summary = "Deletes a tuner host")]
- [Authenticated]
- public class DeleteTunerHost : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Tuner host id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/ListingProviders/Default", "GET")]
- [Authenticated]
- public class GetDefaultListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
- {
- }
-
- [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
- [Authenticated]
- public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
- {
- public bool ValidateLogin { get; set; }
-
- public bool ValidateListings { get; set; }
-
- public string Pw { get; set; }
- }
-
- [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
- [Authenticated]
- public class DeleteListingProvider : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")]
- [Authenticated]
- public class GetLineups : IReturn<List<NameIdPair>>
- {
- [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "Type", Description = "Provider Type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Type { get; set; }
-
- [ApiMember(Name = "Location", Description = "Location", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Location { get; set; }
-
- [ApiMember(Name = "Country", Description = "Country", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Country { get; set; }
- }
-
- [Route("/LiveTv/ListingProviders/SchedulesDirect/Countries", "GET", Summary = "Gets available lineups")]
- [Authenticated]
- public class GetSchedulesDirectCountries
- {
- }
-
- [Route("/LiveTv/ChannelMappingOptions")]
- [Authenticated]
- public class GetChannelMappingOptions
- {
- [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query")]
- public string ProviderId { get; set; }
- }
-
- [Route("/LiveTv/ChannelMappings")]
- [Authenticated]
- public class SetChannelMapping
- {
- [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query")]
- public string ProviderId { get; set; }
-
- public string TunerChannelId { get; set; }
-
- public string ProviderChannelId { get; set; }
- }
-
- public class ChannelMappingOptions
- {
- public List<TunerChannelMapping> TunerChannels { get; set; }
-
- public List<NameIdPair> ProviderChannels { get; set; }
-
- public NameValuePair[] Mappings { get; set; }
-
- public string ProviderName { get; set; }
- }
-
- [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
- public class GetLiveStreamFile
- {
- public string Id { get; set; }
-
- public string Container { get; set; }
- }
-
- [Route("/LiveTv/LiveRecordings/{Id}/stream", "GET", Summary = "Gets a live tv channel")]
- public class GetLiveRecordingFile
- {
- public string Id { get; set; }
- }
-
- [Route("/LiveTv/TunerHosts/Types", "GET")]
- [Authenticated]
- public class GetTunerHostTypes : IReturn<List<NameIdPair>>
- {
- }
-
- [Route("/LiveTv/Tuners/Discvover", "GET")]
- [Authenticated]
- public class DiscoverTuners : IReturn<List<TunerHostInfo>>
- {
- public bool NewDevicesOnly { get; set; }
- }
-
- public class LiveTvService : BaseApiService
- {
- private readonly ILiveTvManager _liveTvManager;
- private readonly IUserManager _userManager;
- private readonly IHttpClient _httpClient;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
- private readonly ISessionContext _sessionContext;
- private readonly IStreamHelper _streamHelper;
- private readonly IMediaSourceManager _mediaSourceManager;
-
- public LiveTvService(
- ILogger<LiveTvService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IMediaSourceManager mediaSourceManager,
- IStreamHelper streamHelper,
- ILiveTvManager liveTvManager,
- IUserManager userManager,
- IHttpClient httpClient,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IAuthorizationContext authContext,
- ISessionContext sessionContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _mediaSourceManager = mediaSourceManager;
- _streamHelper = streamHelper;
- _liveTvManager = liveTvManager;
- _userManager = userManager;
- _httpClient = httpClient;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _authContext = authContext;
- _sessionContext = sessionContext;
- }
-
- public object Get(GetTunerHostTypes request)
- {
- var list = _liveTvManager.GetTunerHostTypes();
- return ToOptimizedResult(list);
- }
-
- public object Get(GetRecordingFolders request)
- {
- var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
- var folders = _liveTvManager.GetRecordingFolders(user);
-
- var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = returnArray,
- TotalRecordCount = returnArray.Count
- };
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetLiveRecordingFile request)
- {
- var path = _liveTvManager.GetEmbyTvActiveRecordingPath(request.Id);
-
- if (string.IsNullOrWhiteSpace(path))
- {
- throw new FileNotFoundException();
- }
-
- var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType(path)
- };
-
- return new ProgressiveFileCopier(_streamHelper, path, outputHeaders, Logger)
- {
- AllowEndOfFile = false
- };
- }
-
- public async Task<object> Get(DiscoverTuners request)
- {
- var result = await _liveTvManager.DiscoverTuners(request.NewDevicesOnly, CancellationToken.None).ConfigureAwait(false);
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetLiveStreamFile request)
- {
- var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(request.Id, CancellationToken.None).ConfigureAwait(false);
-
- var directStreamProvider = liveStreamInfo;
-
- var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file." + request.Container)
- };
-
- return new ProgressiveFileCopier(directStreamProvider, _streamHelper, outputHeaders, Logger)
- {
- AllowEndOfFile = false
- };
- }
-
- public object Get(GetDefaultListingProvider request)
- {
- return ToOptimizedResult(new ListingsProviderInfo());
- }
-
- public async Task<object> Post(SetChannelMapping request)
- {
- return await _liveTvManager.SetChannelMapping(request.ProviderId, request.TunerChannelId, request.ProviderChannelId).ConfigureAwait(false);
- }
-
- public async Task<object> Get(GetChannelMappingOptions request)
- {
- var config = GetConfiguration();
-
- var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(request.ProviderId, i.Id, StringComparison.OrdinalIgnoreCase));
-
- var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
-
- var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(request.ProviderId, CancellationToken.None)
- .ConfigureAwait(false);
-
- var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(request.ProviderId, CancellationToken.None)
- .ConfigureAwait(false);
-
- var mappings = listingsProviderInfo.ChannelMappings;
-
- var result = new ChannelMappingOptions
- {
- 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
- };
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetSchedulesDirectCountries request)
- {
- // https://json.schedulesdirect.org/20141201/available/countries
-
- var response = await _httpClient.Get(new HttpRequestOptions
- {
- Url = "https://json.schedulesdirect.org/20141201/available/countries",
- BufferContent = false
- }).ConfigureAwait(false);
-
- return ResultFactory.GetResult(Request, response, "application/json");
- }
-
- private void AssertUserCanManageLiveTv()
- {
- var user = _sessionContext.GetUser(Request);
-
- if (user == null)
- {
- throw new SecurityException("Anonymous live tv management is not allowed.");
- }
-
- if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
- {
- throw new SecurityException("The current user does not have permission to manage live tv.");
- }
- }
-
- public async Task<object> Post(AddListingProvider request)
- {
- if (request.Pw != null)
- {
- request.Password = GetHashedString(request.Pw);
- }
-
- request.Pw = null;
-
- var result = await _liveTvManager.SaveListingProvider(request, request.ValidateLogin, request.ValidateListings).ConfigureAwait(false);
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the hashed string.
- /// </summary>
- private string GetHashedString(string str)
- {
- // SchedulesDirect requires a SHA1 hash of the user's password
- // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token
- using SHA1 sha = SHA1.Create();
-
- return Hex.Encode(
- sha.ComputeHash(Encoding.UTF8.GetBytes(str)));
- }
-
- public void Delete(DeleteListingProvider request)
- {
- _liveTvManager.DeleteListingsProvider(request.Id);
- }
-
- public async Task<object> Post(AddTunerHost request)
- {
- var result = await _liveTvManager.SaveTunerHost(request).ConfigureAwait(false);
- return ToOptimizedResult(result);
- }
-
- public void Delete(DeleteTunerHost request)
- {
- var config = GetConfiguration();
-
- config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(request.Id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
-
- ServerConfigurationManager.SaveConfiguration("livetv", config);
- }
-
- private LiveTvOptions GetConfiguration()
- {
- return ServerConfigurationManager.GetConfiguration<LiveTvOptions>("livetv");
- }
-
- private void UpdateConfiguration(LiveTvOptions options)
- {
- ServerConfigurationManager.SaveConfiguration("livetv", options);
- }
-
- public async Task<object> Get(GetLineups request)
- {
- var info = await _liveTvManager.GetLineups(request.Type, request.Id, request.Country, request.Location).ConfigureAwait(false);
-
- return ToOptimizedResult(info);
- }
-
- public object Get(GetLiveTvInfo request)
- {
- var info = _liveTvManager.GetLiveTvInfo(CancellationToken.None);
-
- return ToOptimizedResult(info);
- }
-
- public object Get(GetChannels request)
- {
- var options = GetDtoOptions(_authContext, request);
-
- var channelResult = _liveTvManager.GetInternalChannels(new LiveTvChannelQuery
- {
- ChannelType = request.Type,
- UserId = request.UserId,
- StartIndex = request.StartIndex,
- Limit = request.Limit,
- IsFavorite = request.IsFavorite,
- IsLiked = request.IsLiked,
- IsDisliked = request.IsDisliked,
- EnableFavoriteSorting = request.EnableFavoriteSorting,
- IsMovie = request.IsMovie,
- IsSeries = request.IsSeries,
- IsNews = request.IsNews,
- IsKids = request.IsKids,
- IsSports = request.IsSports,
- SortBy = request.GetOrderBy(),
- SortOrder = request.SortOrder ?? SortOrder.Ascending,
- AddCurrentProgram = request.AddCurrentProgram
- }, options, CancellationToken.None);
-
- var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
- RemoveFields(options);
-
- options.AddCurrentProgram = request.AddCurrentProgram;
-
- var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, options, user);
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = returnArray,
- TotalRecordCount = channelResult.TotalRecordCount
- };
-
- return ToOptimizedResult(result);
- }
-
- private void RemoveFields(DtoOptions options)
- {
- var fields = options.Fields.ToList();
-
- fields.Remove(ItemFields.CanDelete);
- fields.Remove(ItemFields.CanDownload);
- fields.Remove(ItemFields.DisplayPreferencesId);
- fields.Remove(ItemFields.Etag);
- options.Fields = fields.ToArray();
- }
-
- public object Get(GetChannel request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetPrograms request)
- {
- var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
- var query = new InternalItemsQuery(user)
- {
- ChannelIds = ApiEntryPoint.Split(request.ChannelIds, ',', true).Select(i => new Guid(i)).ToArray(),
- HasAired = request.HasAired,
- IsAiring = request.IsAiring,
- EnableTotalRecordCount = request.EnableTotalRecordCount
- };
-
- if (!string.IsNullOrEmpty(request.MinStartDate))
- {
- query.MinStartDate = DateTime.Parse(request.MinStartDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- if (!string.IsNullOrEmpty(request.MinEndDate))
- {
- query.MinEndDate = DateTime.Parse(request.MinEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- if (!string.IsNullOrEmpty(request.MaxStartDate))
- {
- query.MaxStartDate = DateTime.Parse(request.MaxStartDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- if (!string.IsNullOrEmpty(request.MaxEndDate))
- {
- query.MaxEndDate = DateTime.Parse(request.MaxEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- query.StartIndex = request.StartIndex;
- query.Limit = request.Limit;
- query.OrderBy = BaseItemsRequest.GetOrderBy(request.SortBy, request.SortOrder);
- query.IsNews = request.IsNews;
- query.IsMovie = request.IsMovie;
- query.IsSeries = request.IsSeries;
- query.IsKids = request.IsKids;
- query.IsSports = request.IsSports;
- query.SeriesTimerId = request.SeriesTimerId;
- query.Genres = (request.Genres ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- query.GenreIds = GetGuids(request.GenreIds);
-
- if (!request.LibrarySeriesId.Equals(Guid.Empty))
- {
- query.IsSeries = true;
-
- if (_libraryManager.GetItemById(request.LibrarySeriesId) is Series series)
- {
- query.Name = series.Name;
- }
- }
-
- var result = await _liveTvManager.GetPrograms(query, GetDtoOptions(_authContext, request), CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetRecommendedPrograms request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var query = new InternalItemsQuery(user)
- {
- IsAiring = request.IsAiring,
- Limit = request.Limit,
- HasAired = request.HasAired,
- IsSeries = request.IsSeries,
- IsMovie = request.IsMovie,
- IsKids = request.IsKids,
- IsNews = request.IsNews,
- IsSports = request.IsSports,
- EnableTotalRecordCount = request.EnableTotalRecordCount
- };
-
- query.GenreIds = GetGuids(request.GenreIds);
-
- var result = _liveTvManager.GetRecommendedPrograms(query, GetDtoOptions(_authContext, request), CancellationToken.None);
-
- return ToOptimizedResult(result);
- }
-
- public object Post(GetPrograms request)
- {
- return Get(request);
- }
-
- public object Get(GetRecordings request)
- {
- var options = GetDtoOptions(_authContext, request);
-
- var result = _liveTvManager.GetRecordings(new RecordingQuery
- {
- ChannelId = request.ChannelId,
- UserId = request.UserId,
- StartIndex = request.StartIndex,
- Limit = request.Limit,
- Status = request.Status,
- SeriesTimerId = request.SeriesTimerId,
- IsInProgress = request.IsInProgress,
- EnableTotalRecordCount = request.EnableTotalRecordCount,
- IsMovie = request.IsMovie,
- IsNews = request.IsNews,
- IsSeries = request.IsSeries,
- IsKids = request.IsKids,
- IsSports = request.IsSports,
- IsLibraryItem = request.IsLibraryItem,
- Fields = request.GetItemFields(),
- ImageTypeLimit = request.ImageTypeLimit,
- EnableImages = request.EnableImages
- }, options);
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetRecordingSeries request)
- {
- return ToOptimizedResult(new QueryResult<BaseItemDto>());
- }
-
- public object Get(GetRecording request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetTimer request)
- {
- var result = await _liveTvManager.GetTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetTimers request)
- {
- var result = await _liveTvManager.GetTimers(new TimerQuery
- {
- ChannelId = request.ChannelId,
- SeriesTimerId = request.SeriesTimerId,
- IsActive = request.IsActive,
- IsScheduled = request.IsScheduled
- }, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public void Delete(DeleteRecording request)
- {
- AssertUserCanManageLiveTv();
-
- _libraryManager.DeleteItem(_libraryManager.GetItemById(request.Id), new DeleteOptions
- {
- DeleteFileLocation = false
- });
- }
-
- public Task Delete(CancelTimer request)
- {
- AssertUserCanManageLiveTv();
-
- return _liveTvManager.CancelTimer(request.Id);
- }
-
- public Task Post(UpdateTimer request)
- {
- AssertUserCanManageLiveTv();
-
- return _liveTvManager.UpdateTimer(request, CancellationToken.None);
- }
-
- public async Task<object> Get(GetSeriesTimers request)
- {
- var result = await _liveTvManager.GetSeriesTimers(new SeriesTimerQuery
- {
- SortOrder = request.SortOrder,
- SortBy = request.SortBy
- }, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetSeriesTimer request)
- {
- var result = await _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public Task Delete(CancelSeriesTimer request)
- {
- AssertUserCanManageLiveTv();
-
- return _liveTvManager.CancelSeriesTimer(request.Id);
- }
-
- public Task Post(UpdateSeriesTimer request)
- {
- AssertUserCanManageLiveTv();
-
- return _liveTvManager.UpdateSeriesTimer(request, CancellationToken.None);
- }
-
- public async Task<object> Get(GetDefaultTimer request)
- {
- if (string.IsNullOrEmpty(request.ProgramId))
- {
- var result = await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
- else
- {
- var result = await _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
- }
-
- public async Task<object> Get(GetProgram request)
- {
- var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
- var result = await _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public Task Post(CreateSeriesTimer request)
- {
- AssertUserCanManageLiveTv();
-
- return _liveTvManager.CreateSeriesTimer(request, CancellationToken.None);
- }
-
- public Task Post(CreateTimer request)
- {
- AssertUserCanManageLiveTv();
-
- return _liveTvManager.CreateTimer(request, CancellationToken.None);
- }
-
- public object Get(GetRecordingGroups request)
- {
- return ToOptimizedResult(new QueryResult<BaseItemDto>());
- }
-
- public object Get(GetRecordingGroup request)
- {
- throw new FileNotFoundException();
- }
-
- public object Get(GetGuideInfo request)
- {
- return ToOptimizedResult(_liveTvManager.GetGuideInfo());
- }
-
- public Task Post(ResetTuner request)
- {
- AssertUserCanManageLiveTv();
-
- return _liveTvManager.ResetTuner(request.Id, CancellationToken.None);
- }
- }
-}
diff --git a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs b/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs
deleted file mode 100644
index 4c608d9a3..000000000
--- a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.LiveTv
-{
- public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders
- {
- private readonly ILogger _logger;
- private readonly string _path;
- private readonly Dictionary<string, string> _outputHeaders;
-
- public bool AllowEndOfFile = true;
-
- private readonly IDirectStreamProvider _directStreamProvider;
- private IStreamHelper _streamHelper;
-
- public ProgressiveFileCopier(IStreamHelper streamHelper, string path, Dictionary<string, string> outputHeaders, ILogger logger)
- {
- _path = path;
- _outputHeaders = outputHeaders;
- _logger = logger;
- _streamHelper = streamHelper;
- }
-
- public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, IStreamHelper streamHelper, Dictionary<string, string> outputHeaders, ILogger logger)
- {
- _directStreamProvider = directStreamProvider;
- _outputHeaders = outputHeaders;
- _logger = logger;
- _streamHelper = streamHelper;
- }
-
- public IDictionary<string, string> Headers => _outputHeaders;
-
- public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
- {
- if (_directStreamProvider != null)
- {
- await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
- return;
- }
-
- var fileOptions = FileOptions.SequentialScan;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (Environment.OSVersion.Platform != PlatformID.Win32NT)
- {
- fileOptions |= FileOptions.Asynchronous;
- }
-
- using (var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions))
- {
- var emptyReadLimit = AllowEndOfFile ? 20 : 100;
- var eofCount = 0;
- while (eofCount < emptyReadLimit)
- {
- int bytesRead;
- bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
-
- if (bytesRead == 0)
- {
- eofCount++;
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- eofCount = 0;
- }
- }
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs
deleted file mode 100644
index d6b5f5195..000000000
--- a/MediaBrowser.Api/LocalizationService.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class GetCultures.
- /// </summary>
- [Route("/Localization/Cultures", "GET", Summary = "Gets known cultures")]
- public class GetCultures : IReturn<CultureDto[]>
- {
- }
-
- /// <summary>
- /// Class GetCountries.
- /// </summary>
- [Route("/Localization/Countries", "GET", Summary = "Gets known countries")]
- public class GetCountries : IReturn<CountryInfo[]>
- {
- }
-
- /// <summary>
- /// Class ParentalRatings.
- /// </summary>
- [Route("/Localization/ParentalRatings", "GET", Summary = "Gets known parental ratings")]
- public class GetParentalRatings : IReturn<ParentalRating[]>
- {
- }
-
- /// <summary>
- /// Class ParentalRatings.
- /// </summary>
- [Route("/Localization/Options", "GET", Summary = "Gets localization options")]
- public class GetLocalizationOptions : IReturn<LocalizationOption[]>
- {
- }
-
- /// <summary>
- /// Class CulturesService.
- /// </summary>
- [Authenticated(AllowBeforeStartupWizard = true)]
- public class LocalizationService : BaseApiService
- {
- /// <summary>
- /// The _localization.
- /// </summary>
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LocalizationService"/> class.
- /// </summary>
- /// <param name="localization">The localization.</param>
- public LocalizationService(
- ILogger<LocalizationService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ILocalizationManager localization)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _localization = localization;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetParentalRatings request)
- {
- var result = _localization.GetParentalRatings();
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetLocalizationOptions request)
- {
- var result = _localization.GetLocalizationOptions();
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetCountries request)
- {
- var result = _localization.GetCountries();
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetCultures request)
- {
- var result = _localization.GetCultures();
-
- return ToOptimizedResult(result);
- }
- }
-
-}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
deleted file mode 100644
index d703bdb05..000000000
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ /dev/null
@@ -1,23 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
- <PropertyGroup>
- <ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
- </PropertyGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
- </ItemGroup>
-
- <ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
- </ItemGroup>
-
- <PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
- </PropertyGroup>
-
-</Project>
diff --git a/MediaBrowser.Api/Movies/CollectionService.cs b/MediaBrowser.Api/Movies/CollectionService.cs
deleted file mode 100644
index e9629439d..000000000
--- a/MediaBrowser.Api/Movies/CollectionService.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System;
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Collections;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Movies
-{
- [Route("/Collections", "POST", Summary = "Creates a new collection")]
- public class CreateCollection : IReturn<CollectionCreationResult>
- {
- [ApiMember(Name = "IsLocked", Description = "Whether or not to lock the new collection.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
- public bool IsLocked { get; set; }
-
- [ApiMember(Name = "Name", Description = "The name of the new collection.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Name { get; set; }
-
- [ApiMember(Name = "ParentId", Description = "Optional - create the collection within a specific folder", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string ParentId { get; set; }
-
- [ApiMember(Name = "Ids", Description = "Item Ids to add to the collection", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
- public string Ids { get; set; }
- }
-
- [Route("/Collections/{Id}/Items", "POST", Summary = "Adds items to a collection")]
- public class AddToCollection : IReturnVoid
- {
- [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Ids { get; set; }
-
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- [Route("/Collections/{Id}/Items", "DELETE", Summary = "Removes items from a collection")]
- public class RemoveFromCollection : IReturnVoid
- {
- [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string Ids { get; set; }
-
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Authenticated]
- public class CollectionService : BaseApiService
- {
- private readonly ICollectionManager _collectionManager;
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
-
- public CollectionService(
- ILogger<CollectionService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ICollectionManager collectionManager,
- IDtoService dtoService,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _collectionManager = collectionManager;
- _dtoService = dtoService;
- _authContext = authContext;
- }
-
- public object Post(CreateCollection request)
- {
- var userId = _authContext.GetAuthorizationInfo(Request).UserId;
-
- var parentId = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId);
-
- var item = _collectionManager.CreateCollection(new CollectionCreationOptions
- {
- IsLocked = request.IsLocked,
- Name = request.Name,
- ParentId = parentId,
- ItemIdList = SplitValue(request.Ids, ','),
- UserIds = new[] { userId }
- });
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
-
- return new CollectionCreationResult
- {
- Id = dto.Id
- };
- }
-
- public void Post(AddToCollection request)
- {
- _collectionManager.AddToCollection(new Guid(request.Id), SplitValue(request.Ids, ','));
- }
-
- public void Delete(RemoveFromCollection request)
- {
- _collectionManager.RemoveFromCollection(new Guid(request.Id), SplitValue(request.Ids, ','));
- }
- }
-}
diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
deleted file mode 100644
index 34cccffa3..000000000
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ /dev/null
@@ -1,414 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-
-namespace MediaBrowser.Api.Movies
-{
- [Route("/Movies/Recommendations", "GET", Summary = "Gets movie recommendations")]
- public class GetMovieRecommendations : IReturn<RecommendationDto[]>, IHasDtoOptions
- {
- [ApiMember(Name = "CategoryLimit", Description = "The max number of categories to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int CategoryLimit { get; set; }
-
- [ApiMember(Name = "ItemLimit", Description = "The max number of items to return per category", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int ItemLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Specify this to localize the search to a specific item or folder. Omit to use the root.
- /// </summary>
- /// <value>The parent id.</value>
- [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ParentId { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- public GetMovieRecommendations()
- {
- CategoryLimit = 5;
- ItemLimit = 8;
- }
-
- public string Fields { get; set; }
- }
-
- /// <summary>
- /// Class MoviesService.
- /// </summary>
- [Authenticated]
- public class MoviesService : BaseApiService
- {
- /// <summary>
- /// The _user manager.
- /// </summary>
- private readonly IUserManager _userManager;
-
- private readonly ILibraryManager _libraryManager;
-
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MoviesService" /> class.
- /// </summary>
- public MoviesService(
- ILogger<MoviesService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _authContext = authContext;
- }
-
- public object Get(GetMovieRecommendations request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = GetRecommendationCategories(user, request.ParentId, request.CategoryLimit, request.ItemLimit, dtoOptions);
-
- return ToOptimizedResult(result);
- }
-
- public QueryResult<BaseItemDto> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request)
- {
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var item = string.IsNullOrEmpty(request.Id) ?
- (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
- _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
- var itemTypes = new List<string> { typeof(Movie).Name };
- if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(typeof(Trailer).Name);
- itemTypes.Add(typeof(LiveTvProgram).Name);
- }
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Limit = request.Limit,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- SimilarTo = item,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
-
- });
-
- var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = returnList,
-
- TotalRecordCount = itemsResult.Count
- };
-
- return result;
- }
-
- private IEnumerable<RecommendationDto> GetRecommendationCategories(User user, string parentId, int categoryLimit, int itemLimit, DtoOptions dtoOptions)
- {
- var categories = new List<RecommendationDto>();
-
- var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
-
- var query = new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[]
- {
- typeof(Movie).Name,
- // typeof(Trailer).Name,
- // typeof(LiveTvProgram).Name
- },
- // IsMovie = true
- OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
- Limit = 7,
- ParentId = parentIdGuid,
- Recursive = true,
- IsPlayed = true,
- DtoOptions = dtoOptions
- };
-
- var recentlyPlayedMovies = _libraryManager.GetItemList(query);
-
- var itemTypes = new List<string> { typeof(Movie).Name };
- if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(typeof(Trailer).Name);
- itemTypes.Add(typeof(LiveTvProgram).Name);
- }
-
- var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
- Limit = 10,
- IsFavoriteOrLiked = true,
- ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
- EnableGroupByMetadataKey = true,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = dtoOptions
- });
-
- var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
- // Get recently played directors
- var recentDirectors = GetDirectors(mostRecentMovies)
- .ToList();
-
- // Get recently played actors
- var recentActors = GetActors(mostRecentMovies)
- .ToList();
-
- var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
- var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
-
- var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
- var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
-
- var categoryTypes = new List<IEnumerator<RecommendationDto>>
- {
- // Give this extra weight
- similarToRecentlyPlayed,
- similarToRecentlyPlayed,
-
- // Give this extra weight
- similarToLiked,
- similarToLiked,
-
- hasDirectorFromRecentlyPlayed,
- hasActorFromRecentlyPlayed
- };
-
- while (categories.Count < categoryLimit)
- {
- var allEmpty = true;
-
- foreach (var category in categoryTypes)
- {
- if (category.MoveNext())
- {
- categories.Add(category.Current);
- allEmpty = false;
-
- if (categories.Count >= categoryLimit)
- {
- break;
- }
- }
- }
-
- if (allEmpty)
- {
- break;
- }
- }
-
- return categories.OrderBy(i => i.RecommendationType);
- }
-
- private IEnumerable<RecommendationDto> GetWithDirector(
- User user,
- IEnumerable<string> names,
- int itemLimit,
- DtoOptions dtoOptions,
- RecommendationType type)
- {
- var itemTypes = new List<string> { typeof(Movie).Name };
- if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(typeof(Trailer).Name);
- itemTypes.Add(typeof(LiveTvProgram).Name);
- }
-
- foreach (var name in names)
- {
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Person = name,
- // Account for duplicates by imdb id, since the database doesn't support this yet
- Limit = itemLimit + 2,
- PersonTypes = new[] { PersonType.Director },
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Select(x => x.First())
- .Take(itemLimit)
- .ToList();
-
- if (items.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
- }
-
- private IEnumerable<RecommendationDto> GetWithActor(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
- {
- var itemTypes = new List<string> { typeof(Movie).Name };
- if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(typeof(Trailer).Name);
- itemTypes.Add(typeof(LiveTvProgram).Name);
- }
-
- foreach (var name in names)
- {
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Person = name,
- // Account for duplicates by imdb id, since the database doesn't support this yet
- Limit = itemLimit + 2,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Select(x => x.First())
- .Take(itemLimit)
- .ToList();
-
- if (items.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
- }
-
- private IEnumerable<RecommendationDto> GetSimilarTo(User user, List<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
- {
- var itemTypes = new List<string> { typeof(Movie).Name };
- if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(typeof(Trailer).Name);
- itemTypes.Add(typeof(LiveTvProgram).Name);
- }
-
- foreach (var item in baselineItems)
- {
- var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Limit = itemLimit,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- SimilarTo = item,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- });
-
- if (similar.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = item.Name,
- CategoryId = item.Id,
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
- }
-
- private IEnumerable<string> GetActors(List<BaseItem> items)
- {
- var people = _libraryManager.GetPeople(new InternalPeopleQuery
- {
- ExcludePersonTypes = new[]
- {
- PersonType.Director
- },
- MaxListOrder = 3
- });
-
- var itemIds = items.Select(i => i.Id).ToList();
-
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
- }
-
- private IEnumerable<string> GetDirectors(List<BaseItem> items)
- {
- var people = _libraryManager.GetPeople(new InternalPeopleQuery
- {
- PersonTypes = new[]
- {
- PersonType.Director
- }
- });
-
- var itemIds = items.Select(i => i.Id).ToList();
-
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
- }
- }
-}
diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs
deleted file mode 100644
index ca9f9d03b..000000000
--- a/MediaBrowser.Api/Movies/TrailersService.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Movies
-{
- [Route("/Trailers", "GET", Summary = "Finds movies and trailers similar to a given trailer.")]
- public class Getrailers : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
- {
- }
-
- /// <summary>
- /// Class TrailersService.
- /// </summary>
- [Authenticated]
- public class TrailersService : BaseApiService
- {
- /// <summary>
- /// The _user manager.
- /// </summary>
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// The logger for the created <see cref="ItemsService"/> instances.
- /// </summary>
- private readonly ILogger<ItemsService> _logger;
-
- private readonly IDtoService _dtoService;
- private readonly ILocalizationManager _localizationManager;
- private readonly IJsonSerializer _json;
- private readonly IAuthorizationContext _authContext;
-
- public TrailersService(
- ILoggerFactory loggerFactory,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- ILocalizationManager localizationManager,
- IJsonSerializer json,
- IAuthorizationContext authContext)
- : base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _localizationManager = localizationManager;
- _json = json;
- _authContext = authContext;
- _logger = loggerFactory.CreateLogger<ItemsService>();
- }
-
- public object Get(Getrailers request)
- {
- var json = _json.SerializeToString(request);
- var getItems = _json.DeserializeFromString<GetItems>(json);
-
- getItems.IncludeItemTypes = "Trailer";
-
- return new ItemsService(
- _logger,
- ServerConfigurationManager,
- ResultFactory,
- _userManager,
- _libraryManager,
- _localizationManager,
- _dtoService,
- _authContext)
- {
- Request = Request,
- }.Get(getItems);
- }
- }
-}
diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs
deleted file mode 100644
index 74d3cce12..000000000
--- a/MediaBrowser.Api/Music/AlbumsService.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Music
-{
- [Route("/Albums/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
- public class GetSimilarAlbums : BaseGetSimilarItemsFromItem
- {
- }
-
- [Route("/Artists/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
- public class GetSimilarArtists : BaseGetSimilarItemsFromItem
- {
- }
-
- [Authenticated]
- public class AlbumsService : BaseApiService
- {
- /// <summary>
- /// The _user manager.
- /// </summary>
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// The _user data repository.
- /// </summary>
- private readonly IUserDataManager _userDataRepository;
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly IItemRepository _itemRepo;
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
-
- public AlbumsService(
- ILogger<AlbumsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- IUserDataManager userDataRepository,
- ILibraryManager libraryManager,
- IItemRepository itemRepo,
- IDtoService dtoService,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _userDataRepository = userDataRepository;
- _libraryManager = libraryManager;
- _itemRepo = itemRepo;
- _dtoService = dtoService;
- _authContext = authContext;
- }
-
- public object Get(GetSimilarArtists request)
- {
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = SimilarItemsHelper.GetSimilarItemsResult(
- dtoOptions,
- _userManager,
- _itemRepo,
- _libraryManager,
- _userDataRepository,
- _dtoService,
- request, new[] { typeof(MusicArtist) },
- SimilarItemsHelper.GetSimiliarityScore);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetSimilarAlbums request)
- {
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = SimilarItemsHelper.GetSimilarItemsResult(
- dtoOptions,
- _userManager,
- _itemRepo,
- _libraryManager,
- _userDataRepository,
- _dtoService,
- request, new[] { typeof(MusicAlbum) },
- GetAlbumSimilarityScore);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the album similarity score.
- /// </summary>
- /// <param name="item1">The item1.</param>
- /// <param name="item1People">The item1 people.</param>
- /// <param name="allPeople">All people.</param>
- /// <param name="item2">The item2.</param>
- /// <returns>System.Int32.</returns>
- private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
- {
- var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
-
- var album1 = (MusicAlbum)item1;
- var album2 = (MusicAlbum)item2;
-
- var artists1 = album1
- .GetAllArtists()
- .DistinctNames()
- .ToList();
-
- var artists2 = new HashSet<string>(
- album2.GetAllArtists().DistinctNames(),
- StringComparer.OrdinalIgnoreCase);
-
- return points + artists1.Where(artists2.Contains).Sum(i => 5);
- }
- }
-}
diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs
deleted file mode 100644
index ebd3eb64a..000000000
--- a/MediaBrowser.Api/Music/InstantMixService.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Music
-{
- [Route("/Songs/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given song")]
- public class GetInstantMixFromSong : BaseGetSimilarItemsFromItem
- {
- }
-
- [Route("/Albums/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given album")]
- public class GetInstantMixFromAlbum : BaseGetSimilarItemsFromItem
- {
- }
-
- [Route("/Playlists/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given playlist")]
- public class GetInstantMixFromPlaylist : BaseGetSimilarItemsFromItem
- {
- }
-
- [Route("/MusicGenres/{Name}/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
- public class GetInstantMixFromMusicGenre : BaseGetSimilarItems
- {
- [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
- }
-
- [Route("/Artists/InstantMix", "GET", Summary = "Creates an instant playlist based on a given artist")]
- public class GetInstantMixFromArtistId : BaseGetSimilarItems
- {
- [ApiMember(Name = "Id", Description = "The artist Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/MusicGenres/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
- public class GetInstantMixFromMusicGenreId : BaseGetSimilarItems
- {
- [ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Items/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given item")]
- public class GetInstantMixFromItem : BaseGetSimilarItemsFromItem
- {
- }
-
- [Authenticated]
- public class InstantMixService : BaseApiService
- {
- private readonly IUserManager _userManager;
-
- private readonly IDtoService _dtoService;
- private readonly ILibraryManager _libraryManager;
- private readonly IMusicManager _musicManager;
- private readonly IAuthorizationContext _authContext;
-
- public InstantMixService(
- ILogger<InstantMixService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- IDtoService dtoService,
- IMusicManager musicManager,
- ILibraryManager libraryManager,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _dtoService = dtoService;
- _musicManager = musicManager;
- _libraryManager = libraryManager;
- _authContext = authContext;
- }
-
- public object Get(GetInstantMixFromItem request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
- return GetResult(items, user, request, dtoOptions);
- }
-
- public object Get(GetInstantMixFromArtistId request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
- return GetResult(items, user, request, dtoOptions);
- }
-
- public object Get(GetInstantMixFromMusicGenreId request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
- return GetResult(items, user, request, dtoOptions);
- }
-
- public object Get(GetInstantMixFromSong request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
- return GetResult(items, user, request, dtoOptions);
- }
-
- public object Get(GetInstantMixFromAlbum request)
- {
- var album = _libraryManager.GetItemById(request.Id);
-
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
-
- return GetResult(items, user, request, dtoOptions);
- }
-
- public object Get(GetInstantMixFromPlaylist request)
- {
- var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
-
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
-
- return GetResult(items, user, request, dtoOptions);
- }
-
- public object Get(GetInstantMixFromMusicGenre request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var items = _musicManager.GetInstantMixFromGenres(new[] { request.Name }, user, dtoOptions);
-
- return GetResult(items, user, request, dtoOptions);
- }
-
- private object GetResult(List<BaseItem> items, User user, BaseGetSimilarItems request, DtoOptions dtoOptions)
- {
- var list = items;
-
- var result = new QueryResult<BaseItemDto>
- {
- TotalRecordCount = list.Count
- };
-
- if (request.Limit.HasValue)
- {
- list = list.Take(request.Limit.Value).ToList();
- }
-
- var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
-
- result.Items = returnList;
-
- return result;
- }
- }
-}
diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs
deleted file mode 100644
index a84556fcc..000000000
--- a/MediaBrowser.Api/PackageService.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Repositories", "GET", Summary = "Gets all package repositories")]
- [Authenticated]
- public class GetRepositories : IReturnVoid
- {
- }
-
- [Route("/Repositories", "POST", Summary = "Sets the enabled and existing package repositories")]
- [Authenticated]
- public class SetRepositories : List<RepositoryInfo>, IReturnVoid
- {
- }
-
- /// <summary>
- /// Class GetPackage.
- /// </summary>
- [Route("/Packages/{Name}", "GET", Summary = "Gets a package, by name or assembly guid")]
- [Authenticated]
- public class GetPackage : IReturn<PackageInfo>
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The name of the package", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "AssemblyGuid", Description = "The guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string AssemblyGuid { get; set; }
- }
-
- /// <summary>
- /// Class GetPackages.
- /// </summary>
- [Route("/Packages", "GET", Summary = "Gets available packages")]
- [Authenticated]
- public class GetPackages : IReturn<PackageInfo[]>
- {
- }
-
- /// <summary>
- /// Class InstallPackage.
- /// </summary>
- [Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")]
- [Authenticated(Roles = "Admin")]
- public class InstallPackage : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "Package name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "AssemblyGuid", Description = "Guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string AssemblyGuid { get; set; }
-
- /// <summary>
- /// Gets or sets the version.
- /// </summary>
- /// <value>The version.</value>
- [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Version { get; set; }
- }
-
- /// <summary>
- /// Class CancelPackageInstallation.
- /// </summary>
- [Route("/Packages/Installing/{Id}", "DELETE", Summary = "Cancels a package installation")]
- [Authenticated(Roles = "Admin")]
- public class CancelPackageInstallation : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Installation Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class PackageService.
- /// </summary>
- public class PackageService : BaseApiService
- {
- private readonly IInstallationManager _installationManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- public PackageService(
- ILogger<PackageService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IInstallationManager installationManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _installationManager = installationManager;
- _serverConfigurationManager = serverConfigurationManager;
- }
-
- public object Get(GetRepositories request)
- {
- var result = _serverConfigurationManager.Configuration.PluginRepositories;
- return ToOptimizedResult(result);
- }
-
- public void Post(SetRepositories request)
- {
- _serverConfigurationManager.Configuration.PluginRepositories = request;
- _serverConfigurationManager.SaveConfiguration();
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetPackage request)
- {
- var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult();
- var result = _installationManager.FilterPackages(
- packages,
- request.Name,
- string.IsNullOrEmpty(request.AssemblyGuid) ? default : Guid.Parse(request.AssemblyGuid)).FirstOrDefault();
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public async Task<object> Get(GetPackages request)
- {
- IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
-
- return ToOptimizedResult(packages.ToArray());
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <exception cref="ResourceNotFoundException"></exception>
- public async Task Post(InstallPackage request)
- {
- var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
- var package = _installationManager.GetCompatibleVersions(
- packages,
- request.Name,
- string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid),
- string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version)).FirstOrDefault();
-
- if (package == null)
- {
- throw new ResourceNotFoundException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Package not found: {0}",
- request.Name));
- }
-
- await _installationManager.InstallPackage(package);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Delete(CancelPackageInstallation request)
- {
- _installationManager.CancelInstallation(new Guid(request.Id));
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
deleted file mode 100644
index 84ed5dcac..000000000
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ /dev/null
@@ -1,1008 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
- /// <summary>
- /// Class BaseStreamingService.
- /// </summary>
- public abstract class BaseStreamingService : BaseApiService
- {
- protected virtual bool EnableOutputInSubFolder => false;
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- protected IUserManager UserManager { get; private set; }
-
- /// <summary>
- /// Gets or sets the library manager.
- /// </summary>
- /// <value>The library manager.</value>
- protected ILibraryManager LibraryManager { get; private set; }
-
- /// <summary>
- /// Gets or sets the iso manager.
- /// </summary>
- /// <value>The iso manager.</value>
- protected IIsoManager IsoManager { get; private set; }
-
- /// <summary>
- /// Gets or sets the media encoder.
- /// </summary>
- /// <value>The media encoder.</value>
- protected IMediaEncoder MediaEncoder { get; private set; }
-
- protected IFileSystem FileSystem { get; private set; }
-
- protected IDlnaManager DlnaManager { get; private set; }
-
- protected IDeviceManager DeviceManager { get; private set; }
-
- protected IMediaSourceManager MediaSourceManager { get; private set; }
-
- protected IJsonSerializer JsonSerializer { get; private set; }
-
- protected IAuthorizationContext AuthorizationContext { get; private set; }
-
- protected EncodingHelper EncodingHelper { get; set; }
-
- /// <summary>
- /// Gets the type of the transcoding job.
- /// </summary>
- /// <value>The type of the transcoding job.</value>
- protected abstract TranscodingJobType TranscodingJobType { get; }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
- /// </summary>
- protected BaseStreamingService(
- ILogger<BaseStreamingService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- EncodingHelper encodingHelper)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- UserManager = userManager;
- LibraryManager = libraryManager;
- IsoManager = isoManager;
- MediaEncoder = mediaEncoder;
- FileSystem = fileSystem;
- DlnaManager = dlnaManager;
- DeviceManager = deviceManager;
- MediaSourceManager = mediaSourceManager;
- JsonSerializer = jsonSerializer;
- AuthorizationContext = authorizationContext;
-
- EncodingHelper = encodingHelper;
- }
-
- /// <summary>
- /// Gets the command line arguments.
- /// </summary>
- protected abstract string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding);
-
- /// <summary>
- /// Gets the output file extension.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <returns>System.String.</returns>
- protected virtual string GetOutputFileExtension(StreamState state)
- {
- return Path.GetExtension(state.RequestedUrl);
- }
-
- /// <summary>
- /// Gets the output file path.
- /// </summary>
- private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension)
- {
- var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}";
-
- var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var ext = outputFileExtension?.ToLowerInvariant();
- var folder = ServerConfigurationManager.GetTranscodePath();
-
- return EnableOutputInSubFolder
- ? Path.Combine(folder, filename, filename + ext)
- : Path.Combine(folder, filename + ext);
- }
-
- protected virtual string GetDefaultEncoderPreset()
- {
- return "superfast";
- }
-
- private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
- {
- if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
- {
- state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
- }
-
- if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
- {
- var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
- {
- OpenToken = state.MediaSource.OpenToken
- }, cancellationTokenSource.Token).ConfigureAwait(false);
-
- EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
-
- if (state.VideoRequest != null)
- {
- EncodingHelper.TryStreamCopy(state);
- }
- }
-
- if (state.MediaSource.BufferMs.HasValue)
- {
- await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Starts the FFMPEG.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="outputPath">The output path.</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <param name="workingDirectory">The working directory.</param>
- /// <returns>Task.</returns>
- protected async Task<TranscodingJob> StartFfMpeg(
- StreamState state,
- string outputPath,
- CancellationTokenSource cancellationTokenSource,
- string workingDirectory = null)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
-
- await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
-
- if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- var auth = AuthorizationContext.GetAuthorizationInfo(Request);
- if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
- {
- ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
-
- throw new ArgumentException("User does not have access to video transcoding");
- }
- }
-
- var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
- 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 = GetCommandLineArguments(outputPath, encodingOptions, state, true),
- WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
-
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- };
-
- var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
- state.Request.PlaySessionId,
- state.MediaSource.LiveStreamId,
- Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- TranscodingJobType,
- process,
- state.Request.DeviceId,
- state,
- cancellationTokenSource);
-
- var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
- Logger.LogInformation(commandLineLogMessage);
-
- var logFilePrefix = "ffmpeg-transcode";
- if (state.VideoRequest != null
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
- ? "ffmpeg-remux" : "ffmpeg-directstream";
- }
-
- var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
-
- // 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, true);
-
- var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
-
- process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
-
- try
- {
- process.Start();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error starting ffmpeg");
-
- ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
-
- throw;
- }
-
- Logger.LogDebug("Launched ffmpeg process");
- state.TranscodingJob = transcodingJob;
-
- // 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);
-
- // Wait for the file to exist before proceeeding
- 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);
- }
-
- Logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
-
- if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
- {
- await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
-
- if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
- {
- await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- if (!transcodingJob.HasExited)
- {
- StartThrottler(state, transcodingJob);
- }
-
- Logger.LogDebug("StartFfMpeg() finished successfully");
-
- return transcodingJob;
- }
-
- private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
- {
- if (EnableThrottling(state))
- {
- transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager, FileSystem);
- state.TranscodingThrottler.Start();
- }
- }
-
- private bool EnableThrottling(StreamState state)
- {
- var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
- // enable throttling when NOT using hardware acceleration
- if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
- {
- return state.InputProtocol == MediaProtocol.File &&
- state.RunTimeTicks.HasValue &&
- state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
- state.IsInputVideo &&
- state.VideoType == VideoType.VideoFile &&
- !EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
- }
-
- return false;
- }
-
- /// <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, TranscodingJob job, StreamState state)
- {
- if (job != null)
- {
- job.HasExited = true;
- }
-
- Logger.LogDebug("Disposing stream resources");
- state.Dispose();
-
- if (process.ExitCode == 0)
- {
- Logger.LogInformation("FFMpeg exited with code 0");
- }
- else
- {
- Logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
- }
-
- process.Dispose();
- }
-
- /// <summary>
- /// Parses the parameters.
- /// </summary>
- /// <param name="request">The request.</param>
- private void ParseParams(StreamRequest request)
- {
- var vals = request.Params.Split(';');
-
- var videoRequest = request as VideoStreamRequest;
-
- for (var i = 0; i < vals.Length; i++)
- {
- var val = vals[i];
-
- 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 != null)
- {
- videoRequest.VideoCodec = val;
- }
-
- break;
- case 5:
- request.AudioCodec = val;
- break;
- case 6:
- if (videoRequest != null)
- {
- videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 7:
- if (videoRequest != null)
- {
- videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 8:
- if (videoRequest != 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 != null)
- {
- videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 12:
- if (videoRequest != null)
- {
- videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 13:
- if (videoRequest != null)
- {
- videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 14:
- request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 15:
- if (videoRequest != null)
- {
- videoRequest.Level = val;
- }
-
- break;
- case 16:
- if (videoRequest != null)
- {
- videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 17:
- if (videoRequest != null)
- {
- videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 18:
- if (videoRequest != 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 != null)
- {
- videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 25:
- if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
- {
- if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
- {
- videoRequest.SubtitleMethod = method;
- }
- }
-
- break;
- case 26:
- request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 27:
- if (videoRequest != null)
- {
- videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 28:
- request.Tag = val;
- break;
- case 29:
- if (videoRequest != null)
- {
- videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 30:
- request.SubtitleCodec = val;
- break;
- case 31:
- if (videoRequest != null)
- {
- videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 32:
- if (videoRequest != null)
- {
- videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 33:
- request.TranscodeReasons = val;
- break;
- }
- }
- }
-
- /// <summary>
- /// Parses query parameters as StreamOptions.
- /// </summary>
- /// <param name="request">The stream request.</param>
- private void ParseStreamOptions(StreamRequest request)
- {
- foreach (var param in Request.QueryString)
- {
- if (char.IsLower(param.Key[0]))
- {
- // This was probably not parsed initially and should be a StreamOptions
- // TODO: This should be incorporated either in the lower framework for parsing requests
- // or the generated URL should correctly serialize it
- request.StreamOptions[param.Key] = param.Value;
- }
- }
- }
-
- /// <summary>
- /// Parses the dlna headers.
- /// </summary>
- /// <param name="request">The request.</param>
- private void ParseDlnaHeaders(StreamRequest request)
- {
- if (!request.StartTimeTicks.HasValue)
- {
- var timeSeek = GetHeader("TimeSeekRange.dlna.org");
-
- request.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
- }
- }
-
- /// <summary>
- /// Parses the time seek header.
- /// </summary>
- private long? ParseTimeSeekHeader(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return null;
- }
-
- const string Npt = "npt=";
- if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException("Invalid timeseek header");
- }
-
- int index = value.IndexOf('-');
- value = index == -1
- ? value.Substring(Npt.Length)
- : value.Substring(Npt.Length, index - Npt.Length);
-
- if (value.IndexOf(':') == -1)
- {
- // 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");
- }
-
- // Parses npt times in the format of '10:19:25.7'
- var tokens = value.Split(new[] { ':' }, 3);
- double secondsSum = 0;
- var timeFactor = 3600;
-
- foreach (var time in tokens)
- {
- if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit))
- {
- secondsSum += digit * timeFactor;
- }
- else
- {
- throw new ArgumentException("Invalid timeseek header");
- }
-
- timeFactor /= 60;
- }
-
- return TimeSpan.FromSeconds(secondsSum).Ticks;
- }
-
- /// <summary>
- /// Gets the state.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>StreamState.</returns>
- protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken)
- {
- ParseDlnaHeaders(request);
-
- if (!string.IsNullOrWhiteSpace(request.Params))
- {
- ParseParams(request);
- }
-
- ParseStreamOptions(request);
-
- var url = Request.PathInfo;
-
- if (string.IsNullOrEmpty(request.AudioCodec))
- {
- request.AudioCodec = EncodingHelper.InferAudioCodec(url);
- }
-
- var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) ||
- string.Equals(GetHeader("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase);
-
- var state = new StreamState(MediaSourceManager, TranscodingJobType)
- {
- Request = request,
- RequestedUrl = url,
- UserAgent = Request.UserAgent,
- EnableDlnaHeaders = enableDlnaHeaders
- };
-
- var auth = AuthorizationContext.GetAuthorizationInfo(Request);
- if (!auth.UserId.Equals(Guid.Empty))
- {
- state.User = UserManager.GetUserById(auth.UserId);
- }
-
- // if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
- // (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
- // (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
- //{
- // state.SegmentLength = 6;
- //}
-
- if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
- {
- state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
- state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
- }
-
- if (!string.IsNullOrWhiteSpace(request.AudioCodec))
- {
- state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
- state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i))
- ?? state.SupportedAudioCodecs.FirstOrDefault();
- }
-
- if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
- {
- state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
- state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToSubtitleCodec(i))
- ?? state.SupportedSubtitleCodecs.FirstOrDefault();
- }
-
- var item = LibraryManager.GetItemById(request.Id);
-
- state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-
- // var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
- // item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
- // if (primaryImage != null)
- //{
- // state.AlbumCoverPath = primaryImage.Path;
- //}
-
- MediaSourceInfo mediaSource = null;
- if (string.IsNullOrWhiteSpace(request.LiveStreamId))
- {
- var currentJob = !string.IsNullOrWhiteSpace(request.PlaySessionId) ?
- ApiEntryPoint.Instance.GetTranscodingJob(request.PlaySessionId)
- : null;
-
- if (currentJob != null)
- {
- mediaSource = currentJob.MediaSource;
- }
-
- if (mediaSource == null)
- {
- var mediaSources = await MediaSourceManager.GetPlaybackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
-
- mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
- ? mediaSources[0]
- : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId));
-
- if (mediaSource == null && Guid.Parse(request.MediaSourceId) == request.Id)
- {
- mediaSource = mediaSources[0];
- }
- }
- }
- else
- {
- var liveStreamInfo = await MediaSourceManager.GetLiveStreamWithDirectStreamProvider(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
- mediaSource = liveStreamInfo.Item1;
- state.DirectStreamProvider = liveStreamInfo.Item2;
- }
-
- var videoRequest = request as VideoStreamRequest;
-
- EncodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
-
- var container = Path.GetExtension(state.RequestedUrl);
-
- if (string.IsNullOrEmpty(container))
- {
- container = request.Container;
- }
-
- if (string.IsNullOrEmpty(container))
- {
- container = request.Static ?
- StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) :
- GetOutputFileExtension(state);
- }
-
- state.OutputContainer = (container ?? string.Empty).TrimStart('.');
-
- state.OutputAudioBitrate = EncodingHelper.GetAudioBitrateParam(state.Request, state.AudioStream);
-
- state.OutputAudioCodec = state.Request.AudioCodec;
-
- state.OutputAudioChannels = EncodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
-
- if (videoRequest != null)
- {
- state.OutputVideoCodec = state.VideoRequest.VideoCodec;
- state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
-
- if (videoRequest != null)
- {
- EncodingHelper.TryStreamCopy(state);
- }
-
- if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- var resolution = ResolutionNormalizer.Normalize(
- state.VideoStream?.BitRate,
- state.VideoStream?.Width,
- state.VideoStream?.Height,
- state.OutputVideoBitrate.Value,
- state.VideoStream?.Codec,
- state.OutputVideoCodec,
- videoRequest.MaxWidth,
- videoRequest.MaxHeight);
-
- videoRequest.MaxWidth = resolution.MaxWidth;
- videoRequest.MaxHeight = resolution.MaxHeight;
- }
- }
-
- ApplyDeviceProfileSettings(state);
-
- var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
- ? GetOutputFileExtension(state)
- : ('.' + state.OutputContainer);
-
- var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
- state.OutputFilePath = GetOutputFilePath(state, encodingOptions, ext);
-
- return state;
- }
-
- private void ApplyDeviceProfileSettings(StreamState state)
- {
- var headers = Request.Headers;
-
- if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId))
- {
- state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
- }
- else if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
- {
- var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
-
- state.DeviceProfile = caps == null ? DlnaManager.GetProfile(headers) : caps.DeviceProfile;
- }
-
- var profile = state.DeviceProfile;
-
- if (profile == null)
- {
- // Don't use settings from the default profile.
- // Only use a specific profile if it was requested.
- return;
- }
-
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
-
- var mediaProfile = state.VideoRequest == null ?
- 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.TargetVideoLevel,
- state.TargetFramerate,
- state.TargetPacketLength,
- state.TargetTimestamp,
- state.IsTargetAnamorphic,
- state.IsTargetInterlaced,
- state.TargetRefFrames,
- state.TargetVideoStreamCount,
- state.TargetAudioStreamCount,
- state.TargetVideoCodecTag,
- state.IsTargetAVC);
-
- if (mediaProfile != null)
- {
- state.MimeType = mediaProfile.MimeType;
- }
-
- if (!state.Request.Static)
- {
- var transcodingProfile = state.VideoRequest == null ?
- profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) :
- profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
-
- if (transcodingProfile != null)
- {
- state.EstimateContentLength = transcodingProfile.EstimateContentLength;
- // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
- state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
-
- if (state.VideoRequest != null)
- {
- state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
- state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
- }
- }
- }
- }
-
- /// <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>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
- protected void AddDlnaHeaders(StreamState state, IDictionary<string, string> responseHeaders, bool isStaticallyStreamed)
- {
- if (!state.EnableDlnaHeaders)
- {
- return;
- }
-
- var profile = state.DeviceProfile;
-
- var transferMode = GetHeader("transferMode.dlna.org");
- responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode;
- responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*";
-
- if (state.RunTimeTicks.HasValue)
- {
- if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
- {
- var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
- responseHeaders["MediaInfo.sec"] = string.Format(
- CultureInfo.InvariantCulture,
- "SEC_Duration={0};",
- Convert.ToInt32(ms));
- }
-
- if (!isStaticallyStreamed && profile != null)
- {
- AddTimeSeekResponseHeaders(state, responseHeaders);
- }
- }
-
- if (profile == null)
- {
- profile = DlnaManager.GetDefaultProfile();
- }
-
- var audioCodec = state.ActualOutputAudioCodec;
-
- if (state.VideoRequest == null)
- {
- responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildAudioHeader(
- state.OutputContainer,
- audioCodec,
- state.OutputAudioBitrate,
- state.OutputAudioSampleRate,
- state.OutputAudioChannels,
- state.OutputAudioBitDepth,
- isStaticallyStreamed,
- state.RunTimeTicks,
- state.TranscodeSeekInfo);
- }
- else
- {
- var videoCodec = state.ActualOutputVideoCodec;
-
- responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildVideoHeader(
- state.OutputContainer,
- videoCodec,
- audioCodec,
- state.OutputWidth,
- state.OutputHeight,
- state.TargetVideoBitDepth,
- state.OutputVideoBitrate,
- state.TargetTimestamp,
- isStaticallyStreamed,
- state.RunTimeTicks,
- state.TargetVideoProfile,
- 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;
- }
- }
-
- private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
- {
- var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
- var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
-
- responseHeaders["TimeSeekRange.dlna.org"] = string.Format(
- CultureInfo.InvariantCulture,
- "npt={0}-{1}/{1}",
- startSeconds,
- runtimeSeconds);
- responseHeaders["X-AvailableSeekRange"] = string.Format(
- CultureInfo.InvariantCulture,
- "1 npt={0}-{1}",
- startSeconds,
- runtimeSeconds);
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
deleted file mode 100644
index 418cd92b3..000000000
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ /dev/null
@@ -1,343 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
- /// <summary>
- /// Class BaseHlsService.
- /// </summary>
- public abstract class BaseHlsService : BaseStreamingService
- {
- public BaseHlsService(
- ILogger<BaseHlsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- EncodingHelper encodingHelper)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- isoManager,
- mediaEncoder,
- fileSystem,
- dlnaManager,
- deviceManager,
- mediaSourceManager,
- jsonSerializer,
- authorizationContext,
- encodingHelper)
- {
- }
-
- /// <summary>
- /// Gets the audio arguments.
- /// </summary>
- protected abstract string GetAudioArguments(StreamState state, EncodingOptions encodingOptions);
-
- /// <summary>
- /// Gets the video arguments.
- /// </summary>
- protected abstract string GetVideoArguments(StreamState state, EncodingOptions encodingOptions);
-
- /// <summary>
- /// Gets the segment file extension.
- /// </summary>
- protected string GetSegmentFileExtension(StreamRequest request)
- {
- var segmentContainer = request.SegmentContainer;
- if (!string.IsNullOrWhiteSpace(segmentContainer))
- {
- return "." + segmentContainer;
- }
-
- return ".ts";
- }
-
- /// <summary>
- /// Gets the type of the transcoding job.
- /// </summary>
- /// <value>The type of the transcoding job.</value>
- protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Hls;
-
- /// <summary>
- /// Processes the request async.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="isLive">if set to <c>true</c> [is live].</param>
- /// <returns>Task{System.Object}.</returns>
- /// <exception cref="ArgumentException">A video bitrate is required
- /// or
- /// An audio bitrate is required</exception>
- protected async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
- {
- var cancellationTokenSource = new CancellationTokenSource();
-
- var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
-
- TranscodingJob job = null;
- var playlist = state.OutputFilePath;
-
- if (!File.Exists(playlist))
- {
- var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- try
- {
- if (!File.Exists(playlist))
- {
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
- job.IsLiveOutput = isLive;
- }
- catch
- {
- state.Dispose();
- throw;
- }
-
- var minSegments = state.MinSegments;
- if (minSegments > 0)
- {
- await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
- }
- finally
- {
- transcodingLock.Release();
- }
- }
-
- if (isLive)
- {
- job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
-
- if (job != null)
- {
- ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
- }
-
- return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
- }
-
- var audioBitrate = state.OutputAudioBitrate ?? 0;
- var videoBitrate = state.OutputVideoBitrate ?? 0;
-
- var baselineStreamBitrate = 64000;
-
- var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate);
-
- job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
-
- if (job != null)
- {
- ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
- }
-
- return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
- }
-
- private string GetLivePlaylistText(string path, int segmentLength)
- {
- using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var reader = new StreamReader(stream);
-
- var text = reader.ReadToEnd();
-
- text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
-
- var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
-
- text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
- // text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
-
- return text;
- }
-
- private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
- {
- var builder = new StringBuilder();
-
- builder.AppendLine("#EXTM3U");
-
- // Pad a little to satisfy the apple hls validator
- var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
-
- // Main stream
- builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture));
- var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
- builder.AppendLine(playlistUrl);
-
- return builder.ToString();
- }
-
- protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
- {
- Logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
-
- while (!cancellationToken.IsCancellationRequested)
- {
- try
- {
- // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
- var fileStream = GetPlaylistFileStream(playlist);
- await using (fileStream.ConfigureAwait(false))
- {
- using var reader = new StreamReader(fileStream);
- var count = 0;
-
- while (!reader.EndOfStream)
- {
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
- if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
- {
- count++;
- if (count >= segmentCount)
- {
- 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);
- }
- }
-
- protected Stream GetPlaylistFileStream(string path)
- {
- return new FileStream(
- path,
- FileMode.Open,
- FileAccess.Read,
- FileShare.ReadWrite,
- IODefaults.FileStreamBufferSize,
- FileOptions.SequentialScan);
- }
-
- protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
- {
- var itsOffsetMs = 0;
-
- var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(CultureInfo.InvariantCulture));
-
- var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
-
- var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
-
- // If isEncoding is true we're actually starting ffmpeg
- var startNumberParam = isEncoding ? GetStartNumber(state).ToString(CultureInfo.InvariantCulture) : "0";
-
- var baseUrlParam = string.Empty;
-
- if (state.Request is GetLiveHlsStream)
- {
- baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
- "hls/" + Path.GetFileNameWithoutExtension(outputPath));
- }
-
- var useGenericSegmenter = true;
- if (useGenericSegmenter)
- {
- var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
-
- var timeDeltaParam = string.Empty;
-
- var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
- if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
- {
- segmentFormat = "mpegts";
- }
-
- baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
-
- return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
- inputModifier,
- EncodingHelper.GetInputArgument(state, encodingOptions),
- threads,
- EncodingHelper.GetMapArgs(state),
- GetVideoArguments(state, encodingOptions),
- GetAudioArguments(state, encodingOptions),
- state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- startNumberParam,
- outputPath,
- outputTsArg,
- timeDeltaParam,
- segmentFormat,
- baseUrlParam
- ).Trim();
- }
-
- // add when stream copying?
- // -avoid_negative_ts make_zero -fflags +genpts
-
- var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
- itsOffset,
- inputModifier,
- EncodingHelper.GetInputArgument(state, encodingOptions),
- threads,
- EncodingHelper.GetMapArgs(state),
- GetVideoArguments(state, encodingOptions),
- GetAudioArguments(state, encodingOptions),
- state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- startNumberParam,
- state.HlsListSize.ToString(CultureInfo.InvariantCulture),
- baseUrlParam,
- outputPath
- ).Trim();
-
- return args;
- }
-
- protected override string GetDefaultEncoderPreset()
- {
- return "veryfast";
- }
-
- protected virtual int GetStartNumber(StreamState state)
- {
- return 0;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
deleted file mode 100644
index fe5f980b1..000000000
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ /dev/null
@@ -1,1230 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
- /// <summary>
- /// Options is needed for chromecast. Threw Head in there since it's related
- /// </summary>
- [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
- [Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
- public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
- {
- public bool EnableAdaptiveBitrateStreaming { get; set; }
-
- public GetMasterHlsVideoPlaylist()
- {
- EnableAdaptiveBitrateStreaming = true;
- }
- }
-
- [Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
- [Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")]
- public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
- {
- public bool EnableAdaptiveBitrateStreaming { get; set; }
-
- public GetMasterHlsAudioPlaylist()
- {
- EnableAdaptiveBitrateStreaming = true;
- }
- }
-
- public interface IMasterHlsRequest
- {
- bool EnableAdaptiveBitrateStreaming { get; set; }
- }
-
- [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
- public class GetVariantHlsVideoPlaylist : VideoStreamRequest
- {
- }
-
- [Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
- public class GetVariantHlsAudioPlaylist : StreamRequest
- {
- }
-
- [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
- public class GetHlsVideoSegment : VideoStreamRequest
- {
- public string PlaylistId { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
- public class GetHlsAudioSegment : StreamRequest
- {
- public string PlaylistId { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- [Authenticated]
- public class DynamicHlsService : BaseHlsService
- {
- public DynamicHlsService(
- ILogger<DynamicHlsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- INetworkManager networkManager,
- EncodingHelper encodingHelper)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- isoManager,
- mediaEncoder,
- fileSystem,
- dlnaManager,
- deviceManager,
- mediaSourceManager,
- jsonSerializer,
- authorizationContext,
- encodingHelper)
- {
- NetworkManager = networkManager;
- }
-
- protected INetworkManager NetworkManager { get; private set; }
-
- public Task<object> Get(GetMasterHlsVideoPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "GET");
- }
-
- public Task<object> Head(GetMasterHlsVideoPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "HEAD");
- }
-
- public Task<object> Get(GetMasterHlsAudioPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "GET");
- }
-
- public Task<object> Head(GetMasterHlsAudioPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "HEAD");
- }
-
- public Task<object> Get(GetVariantHlsVideoPlaylist request)
- {
- return GetVariantPlaylistInternal(request, true, "main");
- }
-
- public Task<object> Get(GetVariantHlsAudioPlaylist request)
- {
- return GetVariantPlaylistInternal(request, false, "main");
- }
-
- public Task<object> Get(GetHlsVideoSegment request)
- {
- return GetDynamicSegment(request, request.SegmentId);
- }
-
- public Task<object> Get(GetHlsAudioSegment request)
- {
- return GetDynamicSegment(request, request.SegmentId);
- }
-
- private async Task<object> GetDynamicSegment(StreamRequest request, string segmentId)
- {
- if ((request.StartTimeTicks ?? 0) > 0)
- {
- throw new ArgumentException("StartTimeTicks is not allowed.");
- }
-
- var cancellationTokenSource = new CancellationTokenSource();
- var cancellationToken = cancellationTokenSource.Token;
-
- var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
-
- var state = await GetState(request, cancellationToken).ConfigureAwait(false);
-
- var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
-
- var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
-
- var segmentExtension = GetSegmentFileExtension(state.Request);
-
- TranscodingJob job = null;
-
- if (File.Exists(segmentPath))
- {
- job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- Logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
- }
-
- var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- var released = false;
- var startTranscoding = false;
-
- try
- {
- if (File.Exists(segmentPath))
- {
- job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- transcodingLock.Release();
- released = true;
- Logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
-
- if (currentTranscodingIndex == null)
- {
- Logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
- startTranscoding = true;
- }
- else if (requestedIndex < currentTranscodingIndex.Value)
- {
- Logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
- startTranscoding = true;
- }
- else if (requestedIndex - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
- {
- Logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", requestedIndex - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, requestedIndex);
- startTranscoding = true;
- }
-
- if (startTranscoding)
- {
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- await ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
-
- if (currentTranscodingIndex.HasValue)
- {
- DeleteLastFile(playlistPath, segmentExtension, 0);
- }
-
- request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex);
-
- state.WaitForPath = segmentPath;
- job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
- }
- catch
- {
- state.Dispose();
- throw;
- }
-
- // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- else
- {
- job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- if (job.TranscodingThrottler != null)
- {
- await job.TranscodingThrottler.UnpauseTranscoding();
- }
- }
- }
- }
- finally
- {
- if (!released)
- {
- transcodingLock.Release();
- }
- }
-
- // Logger.LogInformation("waiting for {0}", segmentPath);
- // while (!File.Exists(segmentPath))
- //{
- // await Task.Delay(50, cancellationToken).ConfigureAwait(false);
- //}
-
- Logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
- }
-
- private const int BufferSize = 81920;
-
- private long GetStartPositionTicks(StreamState state, int requestedIndex)
- {
- double startSeconds = 0;
- var lengths = GetSegmentLengths(state);
-
- if (requestedIndex >= lengths.Length)
- {
- var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
- throw new ArgumentException(msg);
- }
-
- for (var i = 0; i < requestedIndex; i++)
- {
- startSeconds += lengths[i];
- }
-
- var position = TimeSpan.FromSeconds(startSeconds).Ticks;
- return position;
- }
-
- private long GetEndPositionTicks(StreamState state, int requestedIndex)
- {
- double startSeconds = 0;
- var lengths = GetSegmentLengths(state);
-
- if (requestedIndex >= lengths.Length)
- {
- var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
- throw new ArgumentException(msg);
- }
-
- for (var i = 0; i <= requestedIndex; i++)
- {
- startSeconds += lengths[i];
- }
-
- var position = TimeSpan.FromSeconds(startSeconds).Ticks;
- return position;
- }
-
- private double[] GetSegmentLengths(StreamState state)
- {
- var result = new List<double>();
-
- var ticks = state.RunTimeTicks ?? 0;
-
- var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
-
- while (ticks > 0)
- {
- var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
-
- result.Add(TimeSpan.FromTicks(length).TotalSeconds);
-
- ticks -= length;
- }
-
- return result.ToArray();
- }
-
- public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
- {
- var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
-
- if (job == null || job.HasExited)
- {
- return null;
- }
-
- var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
-
- if (file == null)
- {
- return null;
- }
-
- var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
-
- var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
-
- return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
- }
-
- private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
- {
- var file = GetLastTranscodingFile(playlistPath, segmentExtension, FileSystem);
-
- if (file != null)
- {
- DeleteFile(file.FullName, retryCount);
- }
- }
-
- private void DeleteFile(string path, int retryCount)
- {
- if (retryCount >= 5)
- {
- return;
- }
-
- 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);
-
- var task = Task.Delay(100);
- Task.WaitAll(task);
- DeleteFile(path, retryCount + 1);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
- }
- }
-
- private static FileSystemMetadata GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
- {
- var folder = Path.GetDirectoryName(playlist);
-
- var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
-
- 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;
- }
- }
-
- protected override int GetStartNumber(StreamState state)
- {
- return GetStartNumber(state.VideoRequest);
- }
-
- private int GetStartNumber(VideoStreamRequest request)
- {
- var segmentId = "0";
-
- if (request is GetHlsVideoSegment segmentRequest)
- {
- segmentId = segmentRequest.SegmentId;
- }
-
- return int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
- }
-
- private string GetSegmentPath(StreamState state, string playlist, int index)
- {
- var folder = Path.GetDirectoryName(playlist);
-
- var filename = Path.GetFileNameWithoutExtension(playlist);
-
- return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request));
- }
-
- private async Task<object> GetSegmentResult(StreamState state,
- string playlistPath,
- string segmentPath,
- string segmentExtension,
- int segmentIndex,
- TranscodingJob transcodingJob,
- CancellationToken cancellationToken)
- {
- var segmentExists = File.Exists(segmentPath);
- if (segmentExists)
- {
- if (transcodingJob != 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 await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
-
- 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 await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
- }
-
- var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
- if (transcodingJob != null)
- {
- 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)
- {
- if (transcodingJob.HasExited || File.Exists(nextSegmentPath))
- {
- Logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
- return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
- }
- else
- {
- segmentExists = File.Exists(segmentPath);
- if (segmentExists)
- {
- continue; // avoid unnecessary waiting if segment just became available
- }
- }
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
-
- if (!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);
- }
-
- cancellationToken.ThrowIfCancellationRequested();
- }
- else
- {
- Logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
- }
-
- return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
-
- private Task<object> GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJob transcodingJob)
- {
- var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
-
- return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- Path = segmentPath,
- FileShare = FileShare.ReadWrite,
- OnComplete = () =>
- {
- Logger.LogDebug("finished serving {0}", segmentPath);
- if (transcodingJob != null)
- {
- transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
- ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
- }
- }
- });
- }
-
- private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method)
- {
- var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
-
- if (string.IsNullOrEmpty(request.MediaSourceId))
- {
- throw new ArgumentException("MediaSourceId is required");
- }
-
- var playlistText = string.Empty;
-
- if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
- {
- var audioBitrate = state.OutputAudioBitrate ?? 0;
- var videoBitrate = state.OutputVideoBitrate ?? 0;
-
- playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
- }
-
- return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
- }
-
- private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
- {
- var builder = new StringBuilder();
-
- builder.AppendLine("#EXTM3U");
-
- var isLiveStream = state.IsSegmentedLiveStream;
-
- var queryStringIndex = Request.RawUrl.IndexOf('?');
- var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
-
- // from universal audio service
- if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
- {
- queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
- }
- // from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
- {
- queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
- }
-
- // Main stream
- var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
-
- playlistUrl += queryString;
-
- var request = state.Request;
-
- var subtitleStreams = state.MediaSource
- .MediaStreams
- .Where(i => i.IsTextSubtitleStream)
- .ToList();
-
- var subtitleGroup = subtitleStreams.Count > 0 &&
- request is GetMasterHlsVideoPlaylist &&
- (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 != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
- {
- subtitleGroup = null;
- }
-
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- AddSubtitles(state, subtitleStreams, builder);
- }
-
- AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
-
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
- {
- var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
-
- // By default, vary by just 200k
- var variation = GetBitrateVariation(totalBitrate);
-
- var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
-
- variation *= 2;
- newBitrate = totalBitrate - variation;
- variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
- }
-
- return builder.ToString();
- }
-
- private string ReplaceBitrate(string url, int oldValue, int newValue)
- {
- return url.Replace(
- "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
- "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
- StringComparison.OrdinalIgnoreCase);
- }
-
- private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
- {
- var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
-
- foreach (var stream in subtitles)
- {
- const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
-
- var name = stream.DisplayTitle;
-
- var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
- var isForced = stream.IsForced;
-
- var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
- state.Request.MediaSourceId,
- stream.Index.ToString(CultureInfo.InvariantCulture),
- 30.ToString(CultureInfo.InvariantCulture),
- AuthorizationContext.GetAuthorizationInfo(Request).Token);
-
- var line = string.Format(format,
- name,
- isDefault ? "YES" : "NO",
- isForced ? "YES" : "NO",
- url,
- stream.Language ?? "Unknown");
-
- builder.AppendLine(line);
- }
- }
-
- private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream)
- {
- // Within the local network this will likely do more harm than good.
- if (Request.IsLocal || NetworkManager.IsInLocalNetwork(Request.RemoteIp))
- {
- return false;
- }
-
- if (state.Request is IMasterHlsRequest request && !request.EnableAdaptiveBitrateStreaming)
- {
- 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.OutputVideoCodec))
- {
- return false;
- }
-
- if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
- {
- return false;
- }
-
- if (!state.IsOutputVideo)
- {
- return false;
- }
-
- // Having problems in android
- return false;
- // return state.VideoRequest.VideoBitRate.HasValue;
- }
-
- /// <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;
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream.Level.HasValue)
- {
- levelString = state.VideoStream?.Level.ToString();
- }
- else
- {
- levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
- }
-
- if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
- {
- return parsedLevel;
- }
-
- return null;
- }
-
- /// <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 HlsCodecStringFactory.GetAACString(profile);
- }
- else if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringFactory.GetMP3String();
- }
- else if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringFactory.GetAC3String();
- }
- else if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringFactory.GetEAC3String();
- }
-
- 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>
- /// <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;
- }
-
- if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
-
- return HlsCodecStringFactory.GetH264String(profile, level);
- }
- else if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
- return HlsCodecStringFactory.GetH265String(profile, level);
- }
-
- return string.Empty;
- }
-
- /// <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);
- }
-
- // Audio
- string audioCodecs = string.Empty;
- if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
- {
- audioCodecs = GetPlaylistAudioCodecs(state);
- }
-
- StringBuilder codecs = new StringBuilder();
-
- codecs.Append(videoCodecs)
- .Append(',')
- .Append(audioCodecs);
-
- if (codecs.Length > 1)
- {
- 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)
- {
- double? framerate = null;
- if (state.TargetFramerate.HasValue)
- {
- framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
- }
- else if (state.VideoStream?.RealFrameRate != null)
- {
- framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
- }
-
- if (framerate.HasValue)
- {
- builder.Append(",FRAME-RATE=")
- .Append(framerate.Value);
- }
- }
-
- /// <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)
- {
- builder.Append(",RESOLUTION=")
- .Append(state.OutputWidth.GetValueOrDefault())
- .Append('x')
- .Append(state.OutputHeight.GetValueOrDefault());
- }
- }
-
- private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
- {
- builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture))
- .Append(",AVERAGE-BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture));
-
- AppendPlaylistCodecsField(builder, state);
-
- AppendPlaylistResolutionField(builder, state);
-
- AppendPlaylistFramerateField(builder, state);
-
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- builder.Append(",SUBTITLES=\"")
- .Append(subtitleGroup)
- .Append('"');
- }
-
- builder.Append(Environment.NewLine);
- builder.AppendLine(url);
- }
-
- private int GetBitrateVariation(int bitrate)
- {
- // 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;
- }
-
- private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name)
- {
- var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
-
- var segmentLengths = GetSegmentLengths(state);
-
- var builder = new StringBuilder();
-
- builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
- builder.AppendLine("#EXT-X-VERSION:3");
- builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
-
- var queryStringIndex = Request.RawUrl.IndexOf('?');
- var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
-
- // if ((Request.UserAgent ?? string.Empty).IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1)
- //{
- // queryString = string.Empty;
- //}
-
- var index = 0;
-
- foreach (var length in segmentLengths)
- {
- builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
-
- builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
-
- name,
- index.ToString(CultureInfo.InvariantCulture),
- GetSegmentFileExtension(request),
- queryString));
-
- index++;
- }
-
- builder.AppendLine("#EXT-X-ENDLIST");
-
- var playlistText = builder.ToString();
-
- return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
- }
-
- protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
- {
- var audioCodec = EncodingHelper.GetAudioEncoder(state);
-
- if (!state.IsOutputVideo)
- {
- if (EncodingHelper.IsCopyCodec(audioCodec))
- {
- return "-acodec copy";
- }
-
- var audioTranscodeParams = new List<string>();
-
- audioTranscodeParams.Add("-acodec " + audioCodec);
-
- if (state.OutputAudioBitrate.HasValue)
- {
- audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (state.OutputAudioChannels.HasValue)
- {
- audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (state.OutputAudioSampleRate.HasValue)
- {
- audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- audioTranscodeParams.Add("-vn");
- return string.Join(" ", audioTranscodeParams.ToArray());
- }
-
- if (EncodingHelper.IsCopyCodec(audioCodec))
- {
- var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
- {
- return "-codec:a:0 copy -copypriorss:a:0 0";
- }
-
- return "-codec:a:0 copy";
- }
-
- var args = "-codec:a:0 " + audioCodec;
-
- var channels = state.OutputAudioChannels;
-
- if (channels.HasValue)
- {
- args += " -ac " + channels.Value;
- }
-
- var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
- {
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- if (state.OutputAudioSampleRate.HasValue)
- {
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
-
- return args;
- }
-
- protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
- {
- if (!state.IsOutputVideo)
- {
- return string.Empty;
- }
-
- var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var args = "-codec:v:0 " + codec;
-
- // if (state.EnableMpegtsM2TsMode)
- // {
- // args += " -mpegts_m2ts_mode 1";
- // }
-
- // See if we can save come cpu cycles by avoiding encoding
- if (EncodingHelper.IsCopyCodec(codec))
- {
- if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
- {
- string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
- if (!string.IsNullOrEmpty(bitStreamArgs))
- {
- args += " " + bitStreamArgs;
- }
- }
-
- // args += " -flags -global_header";
- }
- else
- {
- var gopArg = string.Empty;
- var keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
- GetStartNumber(state) * state.SegmentLength,
- state.SegmentLength);
-
- var framerate = state.VideoStream?.RealFrameRate;
-
- if (framerate.HasValue)
- {
- // This is to make sure keyframe interval is limited to our segment,
- // as forcing keyframes is not enough.
- // Example: we encoded half of desired length, then codec detected
- // scene cut and inserted a keyframe; next forced keyframe would
- // be created outside of segment, which breaks seeking
- // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
- gopArg = string.Format(
- CultureInfo.InvariantCulture,
- " -g {0} -keyint_min {0} -sc_threshold 0",
- Math.Ceiling(state.SegmentLength * framerate.Value)
- );
- }
-
- args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset());
-
- // Unable to force key frames using these hw encoders, set key frames by GOP
- if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
- {
- args += " " + gopArg;
- }
- else
- {
- args += " " + keyFrameArg + gopArg;
- }
-
- // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
-
- var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
- // This is for graphical subs
- if (hasGraphicalSubs)
- {
- args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
- }
- // Add resolution params, if specified
- else
- {
- args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
- }
-
- // -start_at_zero is necessary to use with -ss when seeking,
- // otherwise the target position cannot be determined.
- if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
- {
- args += " -start_at_zero";
- }
-
- // args += " -flags -global_header";
- }
-
- if (!string.IsNullOrEmpty(state.OutputVideoSync))
- {
- args += " -vsync " + state.OutputVideoSync;
- }
-
- args += EncodingHelper.GetOutputFFlags(state);
-
- return args;
- }
-
- protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
- {
- var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
-
- 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 inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
-
- // If isEncoding is true we're actually starting ffmpeg
- var startNumber = GetStartNumber(state);
- var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
-
- var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty;
-
- var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
-
- var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
- if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
- {
- segmentFormat = "mpegts";
- }
-
- return string.Format(
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
- inputModifier,
- EncodingHelper.GetInputArgument(state, encodingOptions),
- threads,
- mapArgs,
- GetVideoArguments(state, encodingOptions),
- GetAudioArguments(state, encodingOptions),
- state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- segmentFormat,
- startNumberParam,
- outputTsArg,
- outputPath
- ).Trim();
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
deleted file mode 100644
index 8a3d00283..000000000
--- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
- /// <summary>
- /// Class GetHlsAudioSegment.
- /// </summary>
- // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
- //[Authenticated]
- [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")]
- [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")]
- public class GetHlsAudioSegmentLegacy
- {
- // TODO: Deprecate with new iOS app
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- /// <summary>
- /// Class GetHlsVideoSegment.
- /// </summary>
- [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")]
- [Authenticated]
- public class GetHlsPlaylistLegacy
- {
- // TODO: Deprecate with new iOS app
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public string Id { get; set; }
-
- public string PlaylistId { get; set; }
- }
-
- [Route("/Videos/ActiveEncodings", "DELETE")]
- [Authenticated]
- public class StopEncodingProcess
- {
- [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string DeviceId { get; set; }
-
- [ApiMember(Name = "PlaySessionId", Description = "The play session id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string PlaySessionId { get; set; }
- }
-
- /// <summary>
- /// Class GetHlsVideoSegment.
- /// </summary>
- // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
- //[Authenticated]
- [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
- public class GetHlsVideoSegmentLegacy : VideoStreamRequest
- {
- public string PlaylistId { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- public class HlsSegmentService : BaseApiService
- {
- private readonly IFileSystem _fileSystem;
-
- public HlsSegmentService(
- ILogger<HlsSegmentService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IFileSystem fileSystem)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _fileSystem = fileSystem;
- }
-
- public Task<object> Get(GetHlsPlaylistLegacy request)
- {
- var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
- file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file);
-
- return GetFileResult(file, file);
- }
-
- public Task Delete(StopEncodingProcess request)
- {
- return ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, path => true);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetHlsVideoSegmentLegacy request)
- {
- var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
- var transcodeFolderPath = ServerConfigurationManager.GetTranscodePath();
-
- file = Path.Combine(transcodeFolderPath, file);
-
- var normalizedPlaylistId = request.PlaylistId;
-
- var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
- .FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
-
- return GetFileResult(file, playlistPath);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetHlsAudioSegmentLegacy request)
- {
- // TODO: Deprecate with new iOS app
- var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
- file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file);
-
- return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
- }
-
- private Task<object> GetFileResult(string path, string playlistPath)
- {
- var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
-
- return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- Path = path,
- FileShare = FileShare.ReadWrite,
- OnComplete = () =>
- {
- if (transcodingJob != null)
- {
- ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
- }
- }
- });
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
deleted file mode 100644
index 9562f9953..000000000
--- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-using System;
-using System.Globalization;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
- [Route("/Videos/{Id}/live.m3u8", "GET")]
- public class GetLiveHlsStream : VideoStreamRequest
- {
- }
-
- /// <summary>
- /// Class VideoHlsService.
- /// </summary>
- [Authenticated]
- public class VideoHlsService : BaseHlsService
- {
- public VideoHlsService(
- ILogger<VideoHlsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- EncodingHelper encodingHelper)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- isoManager,
- mediaEncoder,
- fileSystem,
- dlnaManager,
- deviceManager,
- mediaSourceManager,
- jsonSerializer,
- authorizationContext,
- encodingHelper)
- {
- }
-
- public Task<object> Get(GetLiveHlsStream request)
- {
- return ProcessRequestAsync(request, true);
- }
-
- /// <summary>
- /// Gets the audio arguments.
- /// </summary>
- protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
- {
- var codec = EncodingHelper.GetAudioEncoder(state);
-
- if (EncodingHelper.IsCopyCodec(codec))
- {
- return "-codec:a:0 copy";
- }
-
- var args = "-codec:a:0 " + codec;
-
- var channels = state.OutputAudioChannels;
-
- if (channels.HasValue)
- {
- args += " -ac " + channels.Value;
- }
-
- var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
- {
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- if (state.OutputAudioSampleRate.HasValue)
- {
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
-
- return args;
- }
-
- /// <summary>
- /// Gets the video arguments.
- /// </summary>
- protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
- {
- if (!state.IsOutputVideo)
- {
- return string.Empty;
- }
-
- var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var args = "-codec:v:0 " + codec;
-
- // if (state.EnableMpegtsM2TsMode)
- // {
- // args += " -mpegts_m2ts_mode 1";
- // }
-
- // See if we can save come cpu cycles by avoiding encoding
- if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
- {
- // if h264_mp4toannexb is ever added, do not use it for live tv
- if (state.VideoStream != null &&
- !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
- {
- string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
- if (!string.IsNullOrEmpty(bitStreamArgs))
- {
- args += " " + bitStreamArgs;
- }
- }
- }
- else
- {
- var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
- state.SegmentLength.ToString(CultureInfo.InvariantCulture));
-
- var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
- args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg;
-
- // Add resolution params, if specified
- if (!hasGraphicalSubs)
- {
- args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
- }
-
- // This is for internal graphical subs
- if (hasGraphicalSubs)
- {
- args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
- }
- }
-
- args += " -flags -global_header";
-
- if (!string.IsNullOrEmpty(state.OutputVideoSync))
- {
- args += " -vsync " + state.OutputVideoSync;
- }
-
- args += EncodingHelper.GetOutputFFlags(state);
-
- return args;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
deleted file mode 100644
index d51787df2..000000000
--- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
- /// <summary>
- /// Class GetAudioStream.
- /// </summary>
- [Route("/Audio/{Id}/stream.{Container}", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/stream.{Container}", "HEAD", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")]
- public class GetAudioStream : StreamRequest
- {
- }
-
- /// <summary>
- /// Class AudioService.
- /// </summary>
- // TODO: In order to autheneticate this in the future, Dlna playback will require updating
- //[Authenticated]
- public class AudioService : BaseProgressiveStreamingService
- {
- public AudioService(
- ILogger<AudioService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IHttpClient httpClient,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- EncodingHelper encodingHelper)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- httpClient,
- userManager,
- libraryManager,
- isoManager,
- mediaEncoder,
- fileSystem,
- dlnaManager,
- deviceManager,
- mediaSourceManager,
- jsonSerializer,
- authorizationContext,
- encodingHelper)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetAudioStream request)
- {
- return ProcessRequest(request, false);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Head(GetAudioStream request)
- {
- return ProcessRequest(request, true);
- }
-
- protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
- {
- return EncodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
deleted file mode 100644
index 2ebf0e420..000000000
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ /dev/null
@@ -1,442 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
- /// <summary>
- /// Class BaseProgressiveStreamingService.
- /// </summary>
- public abstract class BaseProgressiveStreamingService : BaseStreamingService
- {
- protected IHttpClient HttpClient { get; private set; }
-
- public BaseProgressiveStreamingService(
- ILogger<BaseProgressiveStreamingService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IHttpClient httpClient,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- EncodingHelper encodingHelper)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- isoManager,
- mediaEncoder,
- fileSystem,
- dlnaManager,
- deviceManager,
- mediaSourceManager,
- jsonSerializer,
- authorizationContext,
- encodingHelper)
- {
- HttpClient = httpClient;
- }
-
- /// <summary>
- /// Gets the output file extension.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <returns>System.String.</returns>
- protected override string GetOutputFileExtension(StreamState state)
- {
- var ext = base.GetOutputFileExtension(state);
-
- if (!string.IsNullOrEmpty(ext))
- {
- return ext;
- }
-
- var isVideoRequest = state.VideoRequest != null;
-
- // Try to infer based on the desired video codec
- if (isVideoRequest)
- {
- var videoCodec = state.VideoRequest.VideoCodec;
-
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
- {
- return ".ts";
- }
-
- if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
- {
- return ".ogv";
- }
-
- if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
- {
- return ".webm";
- }
-
- if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
- {
- return ".asf";
- }
- }
-
- // Try to infer based on the desired audio codec
- if (!isVideoRequest)
- {
- var audioCodec = state.Request.AudioCodec;
-
- if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".aac";
- }
-
- if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".mp3";
- }
-
- if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".ogg";
- }
-
- if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".wma";
- }
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets the type of the transcoding job.
- /// </summary>
- /// <value>The type of the transcoding job.</value>
- protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Progressive;
-
- /// <summary>
- /// Processes the request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <returns>Task.</returns>
- protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest)
- {
- var cancellationTokenSource = new CancellationTokenSource();
-
- var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
-
- var responseHeaders = new Dictionary<string, string>();
-
- if (request.Static && state.DirectStreamProvider != null)
- {
- AddDlnaHeaders(state, responseHeaders, true);
-
- using (state)
- {
- var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- // TODO: Don't hardcode this
- outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts");
-
- return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None)
- {
- AllowEndOfFile = false
- };
- }
- }
-
- // Static remote stream
- if (request.Static && state.InputProtocol == MediaProtocol.Http)
- {
- AddDlnaHeaders(state, responseHeaders, true);
-
- using (state)
- {
- return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
- }
- }
-
- if (request.Static && state.InputProtocol != MediaProtocol.File)
- {
- throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol));
- }
-
- var outputPath = state.OutputFilePath;
- var outputPathExists = File.Exists(outputPath);
-
- var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob != null;
-
- AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
-
- // Static stream
- if (request.Static)
- {
- var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
-
- using (state)
- {
- if (state.MediaSource.IsInfiniteStream)
- {
- var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- [HeaderNames.ContentType] = contentType
- };
-
-
- return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, CancellationToken.None)
- {
- AllowEndOfFile = false
- };
- }
-
- TimeSpan? cacheDuration = null;
-
- if (!string.IsNullOrEmpty(request.Tag))
- {
- cacheDuration = TimeSpan.FromDays(365);
- }
-
- return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- ResponseHeaders = responseHeaders,
- ContentType = contentType,
- IsHeadRequest = isHeadRequest,
- Path = state.MediaPath,
- CacheDuration = cacheDuration
-
- }).ConfigureAwait(false);
- }
- }
-
- //// Not static but transcode cache file exists
- // if (isTranscodeCached && state.VideoRequest == null)
- //{
- // var contentType = state.GetMimeType(outputPath);
-
- // try
- // {
- // if (transcodingJob != null)
- // {
- // ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
- // }
-
- // return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- // {
- // ResponseHeaders = responseHeaders,
- // ContentType = contentType,
- // IsHeadRequest = isHeadRequest,
- // Path = outputPath,
- // FileShare = FileShare.ReadWrite,
- // OnComplete = () =>
- // {
- // if (transcodingJob != null)
- // {
- // ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
- // }
- // }
-
- // }).ConfigureAwait(false);
- // }
- // finally
- // {
- // state.Dispose();
- // }
- //}
-
- // Need to start ffmpeg
- try
- {
- return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
- }
- catch
- {
- state.Dispose();
-
- throw;
- }
- }
-
- /// <summary>
- /// Gets the static remote stream result.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <returns>Task{System.Object}.</returns>
- private async Task<object> GetStaticRemoteStreamResult(
- StreamState state,
- Dictionary<string, string> responseHeaders,
- bool isHeadRequest,
- CancellationTokenSource cancellationTokenSource)
- {
- var options = new HttpRequestOptions
- {
- Url = state.MediaPath,
- BufferContent = false,
- CancellationToken = cancellationTokenSource.Token
- };
-
- if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
- {
- options.UserAgent = useragent;
- }
-
- var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
-
- responseHeaders[HeaderNames.AcceptRanges] = "none";
-
- // Seeing cases of -1 here
- if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
- {
- responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- if (isHeadRequest)
- {
- using (response)
- {
- return ResultFactory.GetResult(null, Array.Empty<byte>(), response.ContentType, responseHeaders);
- }
- }
-
- var result = new StaticRemoteStreamWriter(response);
-
- result.Headers[HeaderNames.ContentType] = response.ContentType;
-
- // Add the response headers to the result object
- foreach (var header in responseHeaders)
- {
- result.Headers[header.Key] = header.Value;
- }
-
- return result;
- }
-
- /// <summary>
- /// Gets the stream result.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <returns>Task{System.Object}.</returns>
- private async Task<object> GetStreamResult(StreamRequest request, StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
- {
- // Use the command line args with a dummy playlist path
- var outputPath = state.OutputFilePath;
-
- responseHeaders[HeaderNames.AcceptRanges] = "none";
-
- var contentType = state.GetMimeType(outputPath);
-
- // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
- var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
-
- if (contentLength.HasValue)
- {
- responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- // Headers only
- if (isHeadRequest)
- {
- var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
-
- if (streamResult is IHasHeaders hasHeaders)
- {
- if (contentLength.HasValue)
- {
- hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
- }
- else
- {
- hasHeaders.Headers.Remove(HeaderNames.ContentLength);
- }
- }
-
- return streamResult;
- }
-
- var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- try
- {
- TranscodingJob job;
-
- if (!File.Exists(outputPath))
- {
- job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
- }
- else
- {
- job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
- state.Dispose();
- }
-
- var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- [HeaderNames.ContentType] = contentType
- };
-
-
- // Add the response headers to the result object
- foreach (var item in responseHeaders)
- {
- outputHeaders[item.Key] = item.Value;
- }
-
- return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None);
- }
- finally
- {
- transcodingLock.Release();
- }
- }
-
- /// <summary>
- /// Gets the length of the estimated content.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <returns>System.Nullable{System.Int64}.</returns>
- private long? GetEstimatedContentLength(StreamState state)
- {
- var totalBitrate = state.TotalOutputBitrate ?? 0;
-
- if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
- {
- return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
- }
-
- return null;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
deleted file mode 100644
index b70fff128..000000000
--- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
+++ /dev/null
@@ -1,182 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
- public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders
- {
- private readonly IFileSystem _fileSystem;
- private readonly TranscodingJob _job;
- private readonly ILogger _logger;
- private readonly string _path;
- private readonly CancellationToken _cancellationToken;
- private readonly Dictionary<string, string> _outputHeaders;
-
- private long _bytesWritten = 0;
- public long StartPosition { get; set; }
-
- public bool AllowEndOfFile = true;
-
- private readonly IDirectStreamProvider _directStreamProvider;
-
- public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
- {
- _fileSystem = fileSystem;
- _path = path;
- _outputHeaders = outputHeaders;
- _job = job;
- _logger = logger;
- _cancellationToken = cancellationToken;
- }
-
- public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
- {
- _directStreamProvider = directStreamProvider;
- _outputHeaders = outputHeaders;
- _job = job;
- _logger = logger;
- _cancellationToken = cancellationToken;
- }
-
- public IDictionary<string, string> Headers => _outputHeaders;
-
- private Stream GetInputStream(bool allowAsyncFileRead)
- {
- var fileOptions = FileOptions.SequentialScan;
-
- if (allowAsyncFileRead)
- {
- fileOptions |= FileOptions.Asynchronous;
- }
-
- return new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
- }
-
- public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
- {
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
-
- try
- {
- if (_directStreamProvider != null)
- {
- await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
- return;
- }
-
- var eofCount = 0;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- var allowAsyncFileRead = OperatingSystem.Id != OperatingSystemId.Windows;
-
- using (var inputStream = GetInputStream(allowAsyncFileRead))
- {
- if (StartPosition > 0)
- {
- inputStream.Position = StartPosition;
- }
-
- while (eofCount < 20 || !AllowEndOfFile)
- {
- int bytesRead;
- if (allowAsyncFileRead)
- {
- bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
- }
-
- // var position = fs.Position;
- // _logger.LogDebug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
-
- if (bytesRead == 0)
- {
- if (_job == null || _job.HasExited)
- {
- eofCount++;
- }
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- eofCount = 0;
- }
- }
- }
- }
- finally
- {
- if (_job != null)
- {
- ApiEntryPoint.Instance.OnTranscodeEndRequest(_job);
- }
- }
- }
-
- private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
- {
- var array = new byte[IODefaults.CopyToBufferSize];
- int bytesRead;
- int totalBytesRead = 0;
-
- while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
- {
- var bytesToWrite = bytesRead;
-
- if (bytesToWrite > 0)
- {
- await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
- _bytesWritten += bytesRead;
- totalBytesRead += bytesRead;
-
- if (_job != null)
- {
- _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
- }
- }
- }
-
- return totalBytesRead;
- }
-
- private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
- {
- var array = new byte[IODefaults.CopyToBufferSize];
- int bytesRead;
- int totalBytesRead = 0;
-
- while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
- {
- var bytesToWrite = bytesRead;
-
- if (bytesToWrite > 0)
- {
- await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
- _bytesWritten += bytesRead;
- totalBytesRead += bytesRead;
-
- if (_job != null)
- {
- _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
- }
- }
- }
-
- return totalBytesRead;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
deleted file mode 100644
index c3f6b905c..000000000
--- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
- /// <summary>
- /// Class GetVideoStream.
- /// </summary>
- [Route("/Videos/{Id}/stream.mpegts", "GET")]
- [Route("/Videos/{Id}/stream.ts", "GET")]
- [Route("/Videos/{Id}/stream.webm", "GET")]
- [Route("/Videos/{Id}/stream.asf", "GET")]
- [Route("/Videos/{Id}/stream.wmv", "GET")]
- [Route("/Videos/{Id}/stream.ogv", "GET")]
- [Route("/Videos/{Id}/stream.mp4", "GET")]
- [Route("/Videos/{Id}/stream.m4v", "GET")]
- [Route("/Videos/{Id}/stream.mkv", "GET")]
- [Route("/Videos/{Id}/stream.mpeg", "GET")]
- [Route("/Videos/{Id}/stream.mpg", "GET")]
- [Route("/Videos/{Id}/stream.avi", "GET")]
- [Route("/Videos/{Id}/stream.m2ts", "GET")]
- [Route("/Videos/{Id}/stream.3gp", "GET")]
- [Route("/Videos/{Id}/stream.wmv", "GET")]
- [Route("/Videos/{Id}/stream.wtv", "GET")]
- [Route("/Videos/{Id}/stream.mov", "GET")]
- [Route("/Videos/{Id}/stream.iso", "GET")]
- [Route("/Videos/{Id}/stream.flv", "GET")]
- [Route("/Videos/{Id}/stream.rm", "GET")]
- [Route("/Videos/{Id}/stream", "GET")]
- [Route("/Videos/{Id}/stream.ts", "HEAD")]
- [Route("/Videos/{Id}/stream.webm", "HEAD")]
- [Route("/Videos/{Id}/stream.asf", "HEAD")]
- [Route("/Videos/{Id}/stream.wmv", "HEAD")]
- [Route("/Videos/{Id}/stream.ogv", "HEAD")]
- [Route("/Videos/{Id}/stream.mp4", "HEAD")]
- [Route("/Videos/{Id}/stream.m4v", "HEAD")]
- [Route("/Videos/{Id}/stream.mkv", "HEAD")]
- [Route("/Videos/{Id}/stream.mpeg", "HEAD")]
- [Route("/Videos/{Id}/stream.mpg", "HEAD")]
- [Route("/Videos/{Id}/stream.avi", "HEAD")]
- [Route("/Videos/{Id}/stream.3gp", "HEAD")]
- [Route("/Videos/{Id}/stream.wmv", "HEAD")]
- [Route("/Videos/{Id}/stream.wtv", "HEAD")]
- [Route("/Videos/{Id}/stream.m2ts", "HEAD")]
- [Route("/Videos/{Id}/stream.mov", "HEAD")]
- [Route("/Videos/{Id}/stream.iso", "HEAD")]
- [Route("/Videos/{Id}/stream.flv", "HEAD")]
- [Route("/Videos/{Id}/stream", "HEAD")]
- public class GetVideoStream : VideoStreamRequest
- {
- }
-
- /// <summary>
- /// Class VideoService.
- /// </summary>
- // TODO: In order to autheneticate this in the future, Dlna playback will require updating
- //[Authenticated]
- public class VideoService : BaseProgressiveStreamingService
- {
- public VideoService(
- ILogger<VideoService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IHttpClient httpClient,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- EncodingHelper encodingHelper)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- httpClient,
- userManager,
- libraryManager,
- isoManager,
- mediaEncoder,
- fileSystem,
- dlnaManager,
- deviceManager,
- mediaSourceManager,
- jsonSerializer,
- authorizationContext,
- encodingHelper)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetVideoStream request)
- {
- return ProcessRequest(request, false);
- }
-
- /// <summary>
- /// Heads the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Head(GetVideoStream request)
- {
- return ProcessRequest(request, true);
- }
-
- protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
- {
- return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultEncoderPreset());
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
deleted file mode 100644
index 7e2e337ad..000000000
--- a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.Playback
-{
- /// <summary>
- /// Class StaticRemoteStreamWriter.
- /// </summary>
- public class StaticRemoteStreamWriter : IAsyncStreamWriter, IHasHeaders
- {
- /// <summary>
- /// The _input stream.
- /// </summary>
- private readonly HttpResponseInfo _response;
-
- /// <summary>
- /// The _options.
- /// </summary>
- private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
- public StaticRemoteStreamWriter(HttpResponseInfo response)
- {
- _response = response;
- }
-
- /// <summary>
- /// Gets the options.
- /// </summary>
- /// <value>The options.</value>
- public IDictionary<string, string> Headers => _options;
-
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- using (_response)
- {
- await _response.Content.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false);
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs
deleted file mode 100644
index 67c334e48..000000000
--- a/MediaBrowser.Api/Playback/StreamRequest.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.Playback
-{
- /// <summary>
- /// Class StreamRequest.
- /// </summary>
- public class StreamRequest : BaseEncodingJobOptions
- {
- [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string DeviceProfileId { get; set; }
-
- public string Params { get; set; }
-
- public string PlaySessionId { get; set; }
-
- public string Tag { get; set; }
-
- public string SegmentContainer { get; set; }
-
- public int? SegmentLength { get; set; }
-
- public int? MinSegments { get; set; }
- }
-
- public class VideoStreamRequest : StreamRequest
- {
- /// <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;
-
- public bool EnableSubtitlesInManifest { get; set; }
- }
-}
diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs
deleted file mode 100644
index c244b0033..000000000
--- a/MediaBrowser.Api/Playback/StreamState.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Dlna;
-
-namespace MediaBrowser.Api.Playback
-{
- public class StreamState : EncodingJobInfo, IDisposable
- {
- private readonly IMediaSourceManager _mediaSourceManager;
- private bool _disposed = false;
-
- public string RequestedUrl { get; set; }
-
- public StreamRequest Request
- {
- get => (StreamRequest)BaseRequest;
- set
- {
- BaseRequest = value;
-
- IsVideoRequest = VideoRequest != null;
- }
- }
-
- public TranscodingThrottler TranscodingThrottler { get; set; }
-
- public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
-
- public IDirectStreamProvider DirectStreamProvider { get; set; }
-
- public string WaitForPath { get; set; }
-
- public bool IsOutputVideo => Request is VideoStreamRequest;
-
- public int SegmentLength
- {
- get
- {
- if (Request.SegmentLength.HasValue)
- {
- return Request.SegmentLength.Value;
- }
-
- if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
- {
- 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)
- {
- if (IsSegmentedLiveStream)
- {
- return 6;
- }
-
- return 6;
- }
-
- if (IsSegmentedLiveStream)
- {
- return 3;
- }
-
- return 6;
- }
-
- return 3;
- }
- }
-
- public int MinSegments
- {
- get
- {
- if (Request.MinSegments.HasValue)
- {
- return Request.MinSegments.Value;
- }
-
- return SegmentLength >= 10 ? 2 : 3;
- }
- }
-
- public string UserAgent { get; set; }
-
- public bool EstimateContentLength { get; set; }
-
- public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
-
- public bool EnableDlnaHeaders { get; set; }
-
- public DeviceProfile DeviceProfile { get; set; }
-
- public TranscodingJob TranscodingJob { get; set; }
-
- public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
- : base(transcodingType)
- {
- _mediaSourceManager = mediaSourceManager;
- }
-
- public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
- {
- ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
- }
-
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- 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();
- }
-
- TranscodingThrottler?.Dispose();
- }
-
- TranscodingThrottler = null;
- TranscodingJob = null;
-
- _disposed = true;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs
deleted file mode 100644
index d5d78cf37..000000000
--- a/MediaBrowser.Api/Playback/UniversalAudioService.cs
+++ /dev/null
@@ -1,401 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Api.Playback.Hls;
-using MediaBrowser.Api.Playback.Progressive;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
- public class BaseUniversalRequest
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string DeviceId { get; set; }
-
- public Guid UserId { get; set; }
-
- public string AudioCodec { get; set; }
-
- public string Container { get; set; }
-
- public int? MaxAudioChannels { get; set; }
-
- public int? TranscodingAudioChannels { get; set; }
-
- public long? MaxStreamingBitrate { get; set; }
-
- [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public long? StartTimeTicks { get; set; }
-
- public string TranscodingContainer { get; set; }
-
- public string TranscodingProtocol { get; set; }
-
- public int? MaxAudioSampleRate { get; set; }
-
- public int? MaxAudioBitDepth { get; set; }
-
- public bool EnableRedirection { get; set; }
-
- public bool EnableRemoteMedia { get; set; }
-
- public bool BreakOnNonKeyFrames { get; set; }
-
- public BaseUniversalRequest()
- {
- EnableRedirection = true;
- }
- }
-
- [Route("/Audio/{Id}/universal.{Container}", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/universal", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/universal.{Container}", "HEAD", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/universal", "HEAD", Summary = "Gets an audio stream")]
- public class GetUniversalAudioStream : BaseUniversalRequest
- {
- }
-
- [Authenticated]
- public class UniversalAudioService : BaseApiService
- {
- private readonly EncodingHelper _encodingHelper;
- private readonly ILoggerFactory _loggerFactory;
-
- public UniversalAudioService(
- ILogger<UniversalAudioService> logger,
- ILoggerFactory loggerFactory,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IHttpClient httpClient,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IIsoManager isoManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager,
- IJsonSerializer jsonSerializer,
- IAuthorizationContext authorizationContext,
- INetworkManager networkManager,
- EncodingHelper encodingHelper)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- HttpClient = httpClient;
- UserManager = userManager;
- LibraryManager = libraryManager;
- IsoManager = isoManager;
- MediaEncoder = mediaEncoder;
- FileSystem = fileSystem;
- DlnaManager = dlnaManager;
- DeviceManager = deviceManager;
- MediaSourceManager = mediaSourceManager;
- JsonSerializer = jsonSerializer;
- AuthorizationContext = authorizationContext;
- NetworkManager = networkManager;
- _encodingHelper = encodingHelper;
- _loggerFactory = loggerFactory;
- }
-
- protected IHttpClient HttpClient { get; private set; }
-
- protected IUserManager UserManager { get; private set; }
-
- protected ILibraryManager LibraryManager { get; private set; }
-
- protected IIsoManager IsoManager { get; private set; }
-
- protected IMediaEncoder MediaEncoder { get; private set; }
-
- protected IFileSystem FileSystem { get; private set; }
-
- protected IDlnaManager DlnaManager { get; private set; }
-
- protected IDeviceManager DeviceManager { get; private set; }
-
- protected IMediaSourceManager MediaSourceManager { get; private set; }
-
- protected IJsonSerializer JsonSerializer { get; private set; }
-
- protected IAuthorizationContext AuthorizationContext { get; private set; }
-
- protected INetworkManager NetworkManager { get; private set; }
-
- public Task<object> Get(GetUniversalAudioStream request)
- {
- return GetUniversalStream(request, false);
- }
-
- public Task<object> Head(GetUniversalAudioStream request)
- {
- return GetUniversalStream(request, true);
- }
-
- private DeviceProfile GetDeviceProfile(GetUniversalAudioStream request)
- {
- var deviceProfile = new DeviceProfile();
-
- var directPlayProfiles = new List<DirectPlayProfile>();
-
- var containers = (request.Container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-
- foreach (var container in containers)
- {
- var parts = container.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-
- var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
-
- directPlayProfiles.Add(new DirectPlayProfile
- {
- Type = DlnaProfileType.Audio,
- Container = parts[0],
- AudioCodec = audioCodecs
- });
- }
-
- deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
-
- deviceProfile.TranscodingProfiles = new[]
- {
- new TranscodingProfile
- {
- Type = DlnaProfileType.Audio,
- Context = EncodingContext.Streaming,
- Container = request.TranscodingContainer,
- AudioCodec = request.AudioCodec,
- Protocol = request.TranscodingProtocol,
- BreakOnNonKeyFrames = request.BreakOnNonKeyFrames,
- MaxAudioChannels = request.TranscodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
- }
- };
-
- var codecProfiles = new List<CodecProfile>();
- var conditions = new List<ProfileCondition>();
-
- if (request.MaxAudioSampleRate.HasValue)
- {
- // codec profile
- conditions.Add(new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioSampleRate,
- Value = request.MaxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
-
- if (request.MaxAudioBitDepth.HasValue)
- {
- // codec profile
- conditions.Add(new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioBitDepth,
- Value = request.MaxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
-
- if (request.MaxAudioChannels.HasValue)
- {
- // codec profile
- conditions.Add(new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioChannels,
- Value = request.MaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
-
- if (conditions.Count > 0)
- {
- // codec profile
- codecProfiles.Add(new CodecProfile
- {
- Type = CodecType.Audio,
- Container = request.Container,
- Conditions = conditions.ToArray()
- });
- }
-
- deviceProfile.CodecProfiles = codecProfiles.ToArray();
-
- return deviceProfile;
- }
-
- private async Task<object> GetUniversalStream(GetUniversalAudioStream request, bool isHeadRequest)
- {
- var deviceProfile = GetDeviceProfile(request);
-
- AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
-
- var mediaInfoService = new MediaInfoService(
- _loggerFactory.CreateLogger<MediaInfoService>(),
- ServerConfigurationManager,
- ResultFactory,
- MediaSourceManager,
- DeviceManager,
- LibraryManager,
- NetworkManager,
- MediaEncoder,
- UserManager,
- AuthorizationContext)
- {
- Request = Request
- };
-
- var playbackInfoResult = await mediaInfoService.GetPlaybackInfo(new GetPostedPlaybackInfo
- {
- Id = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MaxStreamingBitrate = request.MaxStreamingBitrate,
- StartTimeTicks = request.StartTimeTicks,
- UserId = request.UserId,
- DeviceProfile = deviceProfile,
- MediaSourceId = request.MediaSourceId
- }).ConfigureAwait(false);
-
- var mediaSource = playbackInfoResult.MediaSources[0];
-
- if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
- {
- if (request.EnableRedirection)
- {
- if (mediaSource.IsRemote && request.EnableRemoteMedia)
- {
- return ResultFactory.GetRedirectResult(mediaSource.Path);
- }
- }
- }
-
- var isStatic = mediaSource.SupportsDirectStream;
-
- if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
- {
- var service = new DynamicHlsService(
- _loggerFactory.CreateLogger<DynamicHlsService>(),
- ServerConfigurationManager,
- ResultFactory,
- UserManager,
- LibraryManager,
- IsoManager,
- MediaEncoder,
- FileSystem,
- DlnaManager,
- DeviceManager,
- MediaSourceManager,
- JsonSerializer,
- AuthorizationContext,
- NetworkManager,
- _encodingHelper)
- {
- Request = Request
- };
-
- var transcodingProfile = deviceProfile.TranscodingProfiles[0];
-
- // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
- // TODO: remove this when we switch back to the segment muxer
- var supportedHLSContainers = new[] { "mpegts", "fmp4" };
-
- var newRequest = new GetMasterHlsAudioPlaylist
- {
- AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
- AudioCodec = transcodingProfile.AudioCodec,
- Container = ".m3u8",
- DeviceId = request.DeviceId,
- Id = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MediaSourceId = mediaSource.Id,
- PlaySessionId = playbackInfoResult.PlaySessionId,
- StartTimeTicks = request.StartTimeTicks,
- Static = isStatic,
- // fallback to mpegts if device reports some weird value unsupported by hls
- SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
- AudioSampleRate = request.MaxAudioSampleRate,
- MaxAudioBitDepth = request.MaxAudioBitDepth,
- BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
- };
-
- if (isHeadRequest)
- {
- return await service.Head(newRequest).ConfigureAwait(false);
- }
-
- return await service.Get(newRequest).ConfigureAwait(false);
- }
- else
- {
- var service = new AudioService(
- _loggerFactory.CreateLogger<AudioService>(),
- ServerConfigurationManager,
- ResultFactory,
- HttpClient,
- UserManager,
- LibraryManager,
- IsoManager,
- MediaEncoder,
- FileSystem,
- DlnaManager,
- DeviceManager,
- MediaSourceManager,
- JsonSerializer,
- AuthorizationContext,
- _encodingHelper)
- {
- Request = Request
- };
-
- var newRequest = new GetAudioStream
- {
- AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
- AudioCodec = request.AudioCodec,
- Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
- DeviceId = request.DeviceId,
- Id = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MediaSourceId = mediaSource.Id,
- PlaySessionId = playbackInfoResult.PlaySessionId,
- StartTimeTicks = request.StartTimeTicks,
- Static = isStatic,
- AudioSampleRate = request.MaxAudioSampleRate,
- MaxAudioBitDepth = request.MaxAudioBitDepth,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
- };
-
- if (isHeadRequest)
- {
- return await service.Head(newRequest).ConfigureAwait(false);
- }
-
- return await service.Get(newRequest).ConfigureAwait(false);
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs
deleted file mode 100644
index 5513c0892..000000000
--- a/MediaBrowser.Api/PlaylistService.cs
+++ /dev/null
@@ -1,216 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Playlists;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Playlists", "POST", Summary = "Creates a new playlist")]
- public class CreatePlaylist : IReturn<PlaylistCreationResult>
- {
- [ApiMember(Name = "Name", Description = "The name of the new playlist.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Name { get; set; }
-
- [ApiMember(Name = "Ids", Description = "Item Ids to add to the playlist", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
- public string Ids { get; set; }
-
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "MediaType", Description = "The playlist media type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string MediaType { get; set; }
- }
-
- [Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")]
- public class AddToPlaylist : IReturnVoid
- {
- [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Ids { get; set; }
-
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public Guid UserId { get; set; }
- }
-
- [Route("/Playlists/{Id}/Items/{ItemId}/Move/{NewIndex}", "POST", Summary = "Moves a playlist item")]
- public class MoveItem : IReturnVoid
- {
- [ApiMember(Name = "ItemId", Description = "ItemId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string ItemId { get; set; }
-
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "NewIndex", Description = "NewIndex", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public int NewIndex { get; set; }
- }
-
- [Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")]
- public class RemoveFromPlaylist : IReturnVoid
- {
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
-
- [ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string EntryIds { get; set; }
- }
-
- [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
- public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid Id { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
- }
-
- [Authenticated]
- public class PlaylistService : BaseApiService
- {
- private readonly IPlaylistManager _playlistManager;
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IAuthorizationContext _authContext;
-
- public PlaylistService(
- ILogger<PlaylistService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IDtoService dtoService,
- IPlaylistManager playlistManager,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _dtoService = dtoService;
- _playlistManager = playlistManager;
- _userManager = userManager;
- _libraryManager = libraryManager;
- _authContext = authContext;
- }
-
- public void Post(MoveItem request)
- {
- _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex);
- }
-
- public async Task<object> Post(CreatePlaylist request)
- {
- var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
- {
- Name = request.Name,
- ItemIdList = GetGuids(request.Ids),
- UserId = request.UserId,
- MediaType = request.MediaType
- }).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public void Post(AddToPlaylist request)
- {
- _playlistManager.AddToPlaylist(request.Id, GetGuids(request.Ids), request.UserId);
- }
-
- public void Delete(RemoveFromPlaylist request)
- {
- _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(','));
- }
-
- public object Get(GetPlaylistItems request)
- {
- var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var items = playlist.GetManageableItems().ToArray();
-
- var count = items.Length;
-
- if (request.StartIndex.HasValue)
- {
- items = items.Skip(request.StartIndex.Value).ToArray();
- }
-
- if (request.Limit.HasValue)
- {
- items = items.Take(request.Limit.Value).ToArray();
- }
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
-
- for (int index = 0; index < dtos.Count; index++)
- {
- dtos[index].PlaylistItemId = items[index].Item1.Id;
- }
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = dtos,
- TotalRecordCount = count
- };
-
- return ToOptimizedResult(result);
- }
- }
-}
diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs
deleted file mode 100644
index 7d976ceaa..000000000
--- a/MediaBrowser.Api/PluginService.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class Plugins.
- /// </summary>
- [Route("/Plugins", "GET", Summary = "Gets a list of currently installed plugins")]
- [Authenticated]
- public class GetPlugins : IReturn<PluginInfo[]>
- {
- public bool? IsAppStoreEnabled { get; set; }
- }
-
- /// <summary>
- /// Class UninstallPlugin.
- /// </summary>
- [Route("/Plugins/{Id}", "DELETE", Summary = "Uninstalls a plugin")]
- [Authenticated(Roles = "Admin")]
- public class UninstallPlugin : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class GetPluginConfiguration.
- /// </summary>
- [Route("/Plugins/{Id}/Configuration", "GET", Summary = "Gets a plugin's configuration")]
- [Authenticated]
- public class GetPluginConfiguration
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class UpdatePluginConfiguration.
- /// </summary>
- [Route("/Plugins/{Id}/Configuration", "POST", Summary = "Updates a plugin's configuration")]
- [Authenticated]
- public class UpdatePluginConfiguration : IRequiresRequestStream, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// The raw Http Request Input Stream.
- /// </summary>
- /// <value>The request stream.</value>
- public Stream RequestStream { get; set; }
- }
-
- // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
- // delete all these registration endpoints. They are only kept for compatibility.
- [Route("/Registrations/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
- [Authenticated]
- public class GetRegistration : IReturn<RegistrationInfo>
- {
- [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
- }
-
- /// <summary>
- /// Class GetPluginSecurityInfo.
- /// </summary>
- [Route("/Plugins/SecurityInfo", "GET", Summary = "Gets plugin registration information", IsHidden = true)]
- [Authenticated]
- public class GetPluginSecurityInfo : IReturn<PluginSecurityInfo>
- {
- }
-
- /// <summary>
- /// Class UpdatePluginSecurityInfo.
- /// </summary>
- [Route("/Plugins/SecurityInfo", "POST", Summary = "Updates plugin registration information", IsHidden = true)]
- [Authenticated(Roles = "Admin")]
- public class UpdatePluginSecurityInfo : PluginSecurityInfo, IReturnVoid
- {
- }
-
- [Route("/Plugins/RegistrationRecords/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
- [Authenticated]
- public class GetRegistrationStatus
- {
- [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
- }
-
- // TODO these two classes are only kept for compability with paid plugins and should be removed
- public class RegistrationInfo
- {
- public string Name { get; set; }
-
- public DateTime ExpirationDate { get; set; }
-
- public bool IsTrial { get; set; }
-
- public bool IsRegistered { get; set; }
- }
-
- public class MBRegistrationRecord
- {
- public DateTime ExpirationDate { get; set; }
-
- public bool IsRegistered { get; set; }
-
- public bool RegChecked { get; set; }
-
- public bool RegError { get; set; }
-
- public bool TrialVersion { get; set; }
-
- public bool IsValid { get; set; }
- }
-
- public class PluginSecurityInfo
- {
- public string SupporterKey { get; set; }
-
- public bool IsMBSupporter { get; set; }
- }
- /// <summary>
- /// Class PluginsService.
- /// </summary>
- public class PluginService : BaseApiService
- {
- /// <summary>
- /// The _json serializer.
- /// </summary>
- private readonly IJsonSerializer _jsonSerializer;
-
- /// <summary>
- /// The _app host.
- /// </summary>
- private readonly IApplicationHost _appHost;
- private readonly IInstallationManager _installationManager;
-
- public PluginService(
- ILogger<PluginService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IJsonSerializer jsonSerializer,
- IApplicationHost appHost,
- IInstallationManager installationManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _appHost = appHost;
- _installationManager = installationManager;
- _jsonSerializer = jsonSerializer;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetRegistrationStatus request)
- {
- var record = new MBRegistrationRecord
- {
- IsRegistered = true,
- RegChecked = true,
- TrialVersion = false,
- IsValid = true,
- RegError = false
- };
-
- return ToOptimizedResult(record);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetPlugins request)
- {
- var result = _appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToArray();
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetPluginConfiguration request)
- {
- var guid = new Guid(request.Id);
- var plugin = _appHost.Plugins.First(p => p.Id == guid) as IHasPluginConfiguration;
-
- return ToOptimizedResult(plugin.Configuration);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetPluginSecurityInfo request)
- {
- var result = new PluginSecurityInfo
- {
- IsMBSupporter = true,
- SupporterKey = "IAmTotallyLegit"
- };
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(UpdatePluginSecurityInfo request)
- {
- return Task.CompletedTask;
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public async Task Post(UpdatePluginConfiguration request)
- {
- // We need to parse this manually because we told service stack not to with IRequiresRequestStream
- // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
- var id = Guid.Parse(GetPathValue(1));
-
- if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin))
- {
- throw new FileNotFoundException();
- }
-
- var configuration = (await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, plugin.ConfigurationType).ConfigureAwait(false)) as BasePluginConfiguration;
-
- plugin.UpdateConfiguration(configuration);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Delete(UninstallPlugin request)
- {
- var guid = new Guid(request.Id);
- var plugin = _appHost.Plugins.First(p => p.Id == guid);
-
- _installationManager.UninstallPlugin(plugin);
- }
- }
-}
diff --git a/MediaBrowser.Api/Properties/AssemblyInfo.cs b/MediaBrowser.Api/Properties/AssemblyInfo.cs
deleted file mode 100644
index 078af3e30..000000000
--- a/MediaBrowser.Api/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("MediaBrowser.Api")]
-[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")]
-[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
deleted file mode 100644
index 86b00316a..000000000
--- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
+++ /dev/null
@@ -1,234 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.ScheduledTasks
-{
- /// <summary>
- /// Class GetScheduledTask.
- /// </summary>
- [Route("/ScheduledTasks/{Id}", "GET", Summary = "Gets a scheduled task, by Id")]
- public class GetScheduledTask : IReturn<TaskInfo>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class GetScheduledTasks.
- /// </summary>
- [Route("/ScheduledTasks", "GET", Summary = "Gets scheduled tasks")]
- public class GetScheduledTasks : IReturn<TaskInfo[]>
- {
- [ApiMember(Name = "IsHidden", Description = "Optional filter tasks that are hidden, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsHidden { get; set; }
-
- [ApiMember(Name = "IsEnabled", Description = "Optional filter tasks that are enabled, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsEnabled { get; set; }
- }
-
- /// <summary>
- /// Class StartScheduledTask.
- /// </summary>
- [Route("/ScheduledTasks/Running/{Id}", "POST", Summary = "Starts a scheduled task")]
- public class StartScheduledTask : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class StopScheduledTask.
- /// </summary>
- [Route("/ScheduledTasks/Running/{Id}", "DELETE", Summary = "Stops a scheduled task")]
- public class StopScheduledTask : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class UpdateScheduledTaskTriggers.
- /// </summary>
- [Route("/ScheduledTasks/{Id}/Triggers", "POST", Summary = "Updates the triggers for a scheduled task")]
- public class UpdateScheduledTaskTriggers : List<TaskTriggerInfo>, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the task id.
- /// </summary>
- /// <value>The task id.</value>
- [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class ScheduledTasksService.
- /// </summary>
- [Authenticated(Roles = "Admin")]
- public class ScheduledTaskService : BaseApiService
- {
- /// <summary>
- /// The task manager.
- /// </summary>
- private readonly ITaskManager _taskManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ScheduledTaskService" /> class.
- /// </summary>
- /// <param name="taskManager">The task manager.</param>
- /// <exception cref="ArgumentNullException">taskManager</exception>
- public ScheduledTaskService(
- ILogger<ScheduledTaskService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ITaskManager taskManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _taskManager = taskManager;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>IEnumerable{TaskInfo}.</returns>
- public object Get(GetScheduledTasks request)
- {
- IEnumerable<IScheduledTaskWorker> result = _taskManager.ScheduledTasks
- .OrderBy(i => i.Name);
-
- if (request.IsHidden.HasValue)
- {
- var val = request.IsHidden.Value;
-
- result = result.Where(i =>
- {
- var isHidden = false;
-
- if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
- {
- isHidden = configurableTask.IsHidden;
- }
-
- return isHidden == val;
- });
- }
-
- if (request.IsEnabled.HasValue)
- {
- var val = request.IsEnabled.Value;
-
- result = result.Where(i =>
- {
- var isEnabled = true;
-
- if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
- {
- isEnabled = configurableTask.IsEnabled;
- }
-
- return isEnabled == val;
- });
- }
-
- var infos = result
- .Select(ScheduledTaskHelpers.GetTaskInfo)
- .ToArray();
-
- return ToOptimizedResult(infos);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>IEnumerable{TaskInfo}.</returns>
- /// <exception cref="ResourceNotFoundException">Task not found</exception>
- public object Get(GetScheduledTask request)
- {
- var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
- if (task == null)
- {
- throw new ResourceNotFoundException("Task not found");
- }
-
- var result = ScheduledTaskHelpers.GetTaskInfo(task);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <exception cref="ResourceNotFoundException">Task not found</exception>
- public void Post(StartScheduledTask request)
- {
- var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
- if (task == null)
- {
- throw new ResourceNotFoundException("Task not found");
- }
-
- _taskManager.Execute(task, new TaskOptions());
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <exception cref="ResourceNotFoundException">Task not found</exception>
- public void Delete(StopScheduledTask request)
- {
- var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
- if (task == null)
- {
- throw new ResourceNotFoundException("Task not found");
- }
-
- _taskManager.Cancel(task);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <exception cref="ResourceNotFoundException">Task not found</exception>
- public void Post(UpdateScheduledTaskTriggers request)
- {
- // We need to parse this manually because we told service stack not to with IRequiresRequestStream
- // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
- var id = GetPathValue(1).ToString();
-
- var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.Ordinal));
-
- if (task == null)
- {
- throw new ResourceNotFoundException("Task not found");
- }
-
- task.Triggers = request.ToArray();
- }
- }
-}
diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs
deleted file mode 100644
index 64ee69300..000000000
--- a/MediaBrowser.Api/SearchService.cs
+++ /dev/null
@@ -1,332 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Search;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class GetSearchHints.
- /// </summary>
- [Route("/Search/Hints", "GET", Summary = "Gets search hints based on a search term")]
- public class GetSearchHints : IReturn<SearchHintResult>
- {
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Supply a user id to search within a user's library or omit to search all.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Search characters used to find items.
- /// </summary>
- /// <value>The index by.</value>
- [ApiMember(Name = "SearchTerm", Description = "The search term to filter on", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SearchTerm { get; set; }
-
-
- [ApiMember(Name = "IncludePeople", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludePeople { get; set; }
-
- [ApiMember(Name = "IncludeMedia", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeMedia { get; set; }
-
- [ApiMember(Name = "IncludeGenres", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeGenres { get; set; }
-
- [ApiMember(Name = "IncludeStudios", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeStudios { get; set; }
-
- [ApiMember(Name = "IncludeArtists", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool IncludeArtists { get; set; }
-
- [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string IncludeItemTypes { get; set; }
-
- [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ExcludeItemTypes { get; set; }
-
- [ApiMember(Name = "MediaTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string MediaTypes { get; set; }
-
- public string ParentId { get; set; }
-
- [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsMovie { get; set; }
-
- [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSeries { get; set; }
-
- [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsNews { get; set; }
-
- [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsKids { get; set; }
-
- [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
- public bool? IsSports { get; set; }
-
- public GetSearchHints()
- {
- IncludeArtists = true;
- IncludeGenres = true;
- IncludeMedia = true;
- IncludePeople = true;
- IncludeStudios = true;
- }
- }
-
- /// <summary>
- /// Class SearchService.
- /// </summary>
- [Authenticated]
- public class SearchService : BaseApiService
- {
- /// <summary>
- /// The _search engine.
- /// </summary>
- private readonly ISearchEngine _searchEngine;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IImageProcessor _imageProcessor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SearchService" /> class.
- /// </summary>
- /// <param name="searchEngine">The search engine.</param>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="dtoService">The dto service.</param>
- /// <param name="imageProcessor">The image processor.</param>
- public SearchService(
- ILogger<SearchService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ISearchEngine searchEngine,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IImageProcessor imageProcessor)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _searchEngine = searchEngine;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _imageProcessor = imageProcessor;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetSearchHints request)
- {
- var result = GetSearchHintsAsync(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the search hints async.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{IEnumerable{SearchHintResult}}.</returns>
- private SearchHintResult GetSearchHintsAsync(GetSearchHints request)
- {
- var result = _searchEngine.GetSearchHints(new SearchQuery
- {
- Limit = request.Limit,
- SearchTerm = request.SearchTerm,
- IncludeArtists = request.IncludeArtists,
- IncludeGenres = request.IncludeGenres,
- IncludeMedia = request.IncludeMedia,
- IncludePeople = request.IncludePeople,
- IncludeStudios = request.IncludeStudios,
- StartIndex = request.StartIndex,
- UserId = request.UserId,
- IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true),
- ExcludeItemTypes = ApiEntryPoint.Split(request.ExcludeItemTypes, ',', true),
- MediaTypes = ApiEntryPoint.Split(request.MediaTypes, ',', true),
- ParentId = request.ParentId,
-
- IsKids = request.IsKids,
- IsMovie = request.IsMovie,
- IsNews = request.IsNews,
- IsSeries = request.IsSeries,
- IsSports = request.IsSports
- });
-
- return new SearchHintResult
- {
- TotalRecordCount = result.TotalRecordCount,
-
- SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
- };
- }
-
- /// <summary>
- /// Gets the search hint result.
- /// </summary>
- /// <param name="hintInfo">The hint info.</param>
- /// <returns>SearchHintResult.</returns>
- private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
- {
- var item = hintInfo.Item;
-
- var result = new SearchHint
- {
- Name = item.Name,
- IndexNumber = item.IndexNumber,
- ParentIndexNumber = item.ParentIndexNumber,
- Id = item.Id,
- Type = item.GetClientTypeName(),
- MediaType = item.MediaType,
- MatchedTerm = hintInfo.MatchedTerm,
- RunTimeTicks = item.RunTimeTicks,
- ProductionYear = item.ProductionYear,
- ChannelId = item.ChannelId,
- EndDate = item.EndDate
- };
-
- // legacy
- result.ItemId = result.Id;
-
- if (item.IsFolder)
- {
- result.IsFolder = true;
- }
-
- var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
-
- if (primaryImageTag != null)
- {
- result.PrimaryImageTag = primaryImageTag;
- result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
- }
-
- SetThumbImageInfo(result, item);
- SetBackdropImageInfo(result, item);
-
- switch (item)
- {
- case IHasSeries hasSeries:
- result.Series = hasSeries.SeriesName;
- break;
- case LiveTvProgram program:
- result.StartDate = program.StartDate;
- break;
- case Series series:
- if (series.Status.HasValue)
- {
- result.Status = series.Status.Value.ToString();
- }
-
- break;
- case MusicAlbum album:
- result.Artists = album.Artists;
- result.AlbumArtist = album.AlbumArtist;
- break;
- case Audio song:
- result.AlbumArtist = song.AlbumArtists.FirstOrDefault();
- result.Artists = song.Artists;
-
- MusicAlbum musicAlbum = song.AlbumEntity;
-
- if (musicAlbum != null)
- {
- result.Album = musicAlbum.Name;
- result.AlbumId = musicAlbum.Id;
- }
- else
- {
- result.Album = song.Album;
- }
-
- break;
- }
-
- if (!item.ChannelId.Equals(Guid.Empty))
- {
- var channel = _libraryManager.GetItemById(item.ChannelId);
- result.ChannelName = channel?.Name;
- }
-
- return result;
- }
-
- private void SetThumbImageInfo(SearchHint hint, BaseItem item)
- {
- var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
-
- if (itemWithImage == null && item is Episode)
- {
- itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
- }
-
- if (itemWithImage == null)
- {
- itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
- }
-
- if (itemWithImage != null)
- {
- var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
-
- if (tag != null)
- {
- hint.ThumbImageTag = tag;
- hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
- }
- }
- }
-
- private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
- {
- var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
- ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
-
- if (itemWithImage != null)
- {
- var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
-
- if (tag != null)
- {
- hint.BackdropImageTag = tag;
- hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
- }
- }
- }
-
- private T GetParentWithImage<T>(BaseItem item, ImageType type)
- where T : BaseItem
- {
- return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
- }
- }
-}
diff --git a/MediaBrowser.Api/Sessions/ApiKeyService.cs b/MediaBrowser.Api/Sessions/ApiKeyService.cs
deleted file mode 100644
index 5102ce0a7..000000000
--- a/MediaBrowser.Api/Sessions/ApiKeyService.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System;
-using System.Globalization;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Sessions
-{
- [Route("/Auth/Keys", "GET")]
- [Authenticated(Roles = "Admin")]
- public class GetKeys
- {
- }
-
- [Route("/Auth/Keys/{Key}", "DELETE")]
- [Authenticated(Roles = "Admin")]
- public class RevokeKey
- {
- [ApiMember(Name = "Key", Description = "Authentication key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Key { get; set; }
- }
-
- [Route("/Auth/Keys", "POST")]
- [Authenticated(Roles = "Admin")]
- public class CreateKey
- {
- [ApiMember(Name = "App", Description = "Name of the app using the authentication key", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string App { get; set; }
- }
-
- public class ApiKeyService : BaseApiService
- {
- private readonly ISessionManager _sessionManager;
-
- private readonly IAuthenticationRepository _authRepo;
-
- private readonly IServerApplicationHost _appHost;
-
- public ApiKeyService(
- ILogger<ApiKeyService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ISessionManager sessionManager,
- IServerApplicationHost appHost,
- IAuthenticationRepository authRepo)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _sessionManager = sessionManager;
- _authRepo = authRepo;
- _appHost = appHost;
- }
-
- public void Delete(RevokeKey request)
- {
- _sessionManager.RevokeToken(request.Key);
- }
-
- public void Post(CreateKey request)
- {
- _authRepo.Create(new AuthenticationInfo
- {
- AppName = request.App,
- AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- DateCreated = DateTime.UtcNow,
- DeviceId = _appHost.SystemId,
- DeviceName = _appHost.FriendlyName,
- AppVersion = _appHost.ApplicationVersionString
- });
- }
-
- public object Get(GetKeys request)
- {
- var result = _authRepo.Get(new AuthenticationInfoQuery
- {
- HasUser = false
- });
-
- return result;
- }
- }
-}
diff --git a/MediaBrowser.Api/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs
deleted file mode 100644
index 50adc5698..000000000
--- a/MediaBrowser.Api/Sessions/SessionService.cs
+++ /dev/null
@@ -1,499 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Sessions
-{
- /// <summary>
- /// Class GetSessions.
- /// </summary>
- [Route("/Sessions", "GET", Summary = "Gets a list of sessions")]
- [Authenticated]
- public class GetSessions : IReturn<SessionInfo[]>
- {
- [ApiMember(Name = "ControllableByUserId", Description = "Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid ControllableByUserId { get; set; }
-
- [ApiMember(Name = "DeviceId", Description = "Filter by device Id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string DeviceId { get; set; }
-
- public int? ActiveWithinSeconds { get; set; }
- }
-
- /// <summary>
- /// Class DisplayContent.
- /// </summary>
- [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")]
- [Authenticated]
- public class DisplayContent : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// Artist, Genre, Studio, Person, or any kind of BaseItem.
- /// </summary>
- /// <value>The type of the item.</value>
- [ApiMember(Name = "ItemType", Description = "The type of item to browse to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string ItemType { get; set; }
-
- /// <summary>
- /// Artist name, genre name, item Id, etc.
- /// </summary>
- /// <value>The item identifier.</value>
- [ApiMember(Name = "ItemId", Description = "The Id of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string ItemId { get; set; }
-
- /// <summary>
- /// Gets or sets the name of the item.
- /// </summary>
- /// <value>The name of the item.</value>
- [ApiMember(Name = "ItemName", Description = "The name of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string ItemName { get; set; }
- }
-
- [Route("/Sessions/{Id}/Playing", "POST", Summary = "Instructs a session to play an item")]
- [Authenticated]
- public class Play : PlayRequest
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- [Route("/Sessions/{Id}/Playing/{Command}", "POST", Summary = "Issues a playstate command to a client")]
- [Authenticated]
- public class SendPlaystateCommand : PlaystateRequest, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- [Route("/Sessions/{Id}/System/{Command}", "POST", Summary = "Issues a system command to a client")]
- [Authenticated]
- public class SendSystemCommand : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the command.
- /// </summary>
- /// <value>The play command.</value>
- [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Command { get; set; }
- }
-
- [Route("/Sessions/{Id}/Command/{Command}", "POST", Summary = "Issues a system command to a client")]
- [Authenticated]
- public class SendGeneralCommand : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the command.
- /// </summary>
- /// <value>The play command.</value>
- [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Command { get; set; }
- }
-
- [Route("/Sessions/{Id}/Command", "POST", Summary = "Issues a system command to a client")]
- [Authenticated]
- public class SendFullGeneralCommand : GeneralCommand, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- [Route("/Sessions/{Id}/Message", "POST", Summary = "Issues a command to a client to display a message to the user")]
- [Authenticated]
- public class SendMessageCommand : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- [ApiMember(Name = "Text", Description = "The message text.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Text { get; set; }
-
- [ApiMember(Name = "Header", Description = "The message header.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Header { get; set; }
-
- [ApiMember(Name = "TimeoutMs", Description = "The message timeout. If omitted the user will have to confirm viewing the message.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public long? TimeoutMs { get; set; }
- }
-
- [Route("/Sessions/{Id}/Users/{UserId}", "POST", Summary = "Adds an additional user to a session")]
- [Authenticated]
- public class AddUserToSession : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "UserId Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string UserId { get; set; }
- }
-
- [Route("/Sessions/{Id}/Users/{UserId}", "DELETE", Summary = "Removes an additional user from a session")]
- [Authenticated]
- public class RemoveUserFromSession : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string UserId { get; set; }
- }
-
- [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
- [Authenticated]
- public class PostCapabilities : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Id { get; set; }
-
- [ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string PlayableMediaTypes { get; set; }
-
- [ApiMember(Name = "SupportedCommands", Description = "A list of supported remote control commands, comma delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string SupportedCommands { get; set; }
-
- [ApiMember(Name = "SupportsMediaControl", Description = "Determines whether media can be played remotely.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
- public bool SupportsMediaControl { get; set; }
-
- [ApiMember(Name = "SupportsSync", Description = "Determines whether sync is supported.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
- public bool SupportsSync { get; set; }
-
- [ApiMember(Name = "SupportsPersistentIdentifier", Description = "Determines whether the device supports a unique identifier.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
- public bool SupportsPersistentIdentifier { get; set; }
-
- public PostCapabilities()
- {
- SupportsPersistentIdentifier = true;
- }
- }
-
- [Route("/Sessions/Capabilities/Full", "POST", Summary = "Updates capabilities for a device")]
- [Authenticated]
- public class PostFullCapabilities : ClientCapabilities, IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string Id { get; set; }
- }
-
- [Route("/Sessions/Viewing", "POST", Summary = "Reports that a session is viewing an item")]
- [Authenticated]
- public class ReportViewing : IReturnVoid
- {
- [ApiMember(Name = "SessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string SessionId { get; set; }
-
- [ApiMember(Name = "ItemId", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string ItemId { get; set; }
- }
-
- [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")]
- [Authenticated]
- public class ReportSessionEnded : IReturnVoid
- {
- }
-
- [Route("/Auth/Providers", "GET")]
- [Authenticated(Roles = "Admin")]
- public class GetAuthProviders : IReturn<NameIdPair[]>
- {
- }
-
- [Route("/Auth/PasswordResetProviders", "GET")]
- [Authenticated(Roles = "Admin")]
- public class GetPasswordResetProviders : IReturn<NameIdPair[]>
- {
- }
-
- /// <summary>
- /// Class SessionsService.
- /// </summary>
- public class SessionService : BaseApiService
- {
- /// <summary>
- /// The session manager.
- /// </summary>
- private readonly ISessionManager _sessionManager;
-
- private readonly IUserManager _userManager;
- private readonly IAuthorizationContext _authContext;
- private readonly IDeviceManager _deviceManager;
- private readonly ISessionContext _sessionContext;
-
- public SessionService(
- ILogger<SessionService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ISessionManager sessionManager,
- IUserManager userManager,
- IAuthorizationContext authContext,
- IDeviceManager deviceManager,
- ISessionContext sessionContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _sessionManager = sessionManager;
- _userManager = userManager;
- _authContext = authContext;
- _deviceManager = deviceManager;
- _sessionContext = sessionContext;
- }
-
- public object Get(GetAuthProviders request)
- {
- return _userManager.GetAuthenticationProviders();
- }
-
- public object Get(GetPasswordResetProviders request)
- {
- return _userManager.GetPasswordResetProviders();
- }
-
- public void Post(ReportSessionEnded request)
- {
- var auth = _authContext.GetAuthorizationInfo(Request);
-
- _sessionManager.Logout(auth.Token);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetSessions request)
- {
- var result = _sessionManager.Sessions;
-
- if (!string.IsNullOrEmpty(request.DeviceId))
- {
- result = result.Where(i => string.Equals(i.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase));
- }
-
- if (!request.ControllableByUserId.Equals(Guid.Empty))
- {
- result = result.Where(i => i.SupportsRemoteControl);
-
- var user = _userManager.GetUserById(request.ControllableByUserId);
-
- if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
- {
- result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId));
- }
-
- if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
- {
- result = result.Where(i => !i.UserId.Equals(Guid.Empty));
- }
-
- if (request.ActiveWithinSeconds.HasValue && request.ActiveWithinSeconds.Value > 0)
- {
- var minActiveDate = DateTime.UtcNow.AddSeconds(0 - request.ActiveWithinSeconds.Value);
- result = result.Where(i => i.LastActivityDate >= minActiveDate);
- }
-
- result = result.Where(i =>
- {
- var deviceId = i.DeviceId;
-
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- if (!_deviceManager.CanAccessDevice(user, deviceId))
- {
- return false;
- }
- }
-
- return true;
- });
- }
-
- return ToOptimizedResult(result.ToArray());
- }
-
- public Task Post(SendPlaystateCommand request)
- {
- return _sessionManager.SendPlaystateCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(DisplayContent request)
- {
- var command = new BrowseRequest
- {
- ItemId = request.ItemId,
- ItemName = request.ItemName,
- ItemType = request.ItemType
- };
-
- return _sessionManager.SendBrowseCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(SendSystemCommand request)
- {
- var name = request.Command;
- if (Enum.TryParse(name, true, out GeneralCommandType commandType))
- {
- name = commandType.ToString();
- }
-
- var currentSession = GetSession(_sessionContext);
- var command = new GeneralCommand
- {
- Name = name,
- ControllingUserId = currentSession.UserId
- };
-
- return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(SendMessageCommand request)
- {
- var command = new MessageCommand
- {
- Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header,
- TimeoutMs = request.TimeoutMs,
- Text = request.Text
- };
-
- return _sessionManager.SendMessageCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(Play request)
- {
- return _sessionManager.SendPlayCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None);
- }
-
- public Task Post(SendGeneralCommand request)
- {
- var currentSession = GetSession(_sessionContext);
-
- var command = new GeneralCommand
- {
- Name = request.Command,
- ControllingUserId = currentSession.UserId
- };
-
- return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
- }
-
- public Task Post(SendFullGeneralCommand request)
- {
- var currentSession = GetSession(_sessionContext);
-
- request.ControllingUserId = currentSession.UserId;
-
- return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, request, CancellationToken.None);
- }
-
- public void Post(AddUserToSession request)
- {
- _sessionManager.AddAdditionalUser(request.Id, new Guid(request.UserId));
- }
-
- public void Delete(RemoveUserFromSession request)
- {
- _sessionManager.RemoveAdditionalUser(request.Id, new Guid(request.UserId));
- }
-
- public void Post(PostCapabilities request)
- {
- if (string.IsNullOrWhiteSpace(request.Id))
- {
- request.Id = GetSession(_sessionContext).Id;
- }
-
- _sessionManager.ReportCapabilities(request.Id, new ClientCapabilities
- {
- PlayableMediaTypes = SplitValue(request.PlayableMediaTypes, ','),
- SupportedCommands = SplitValue(request.SupportedCommands, ','),
- SupportsMediaControl = request.SupportsMediaControl,
- SupportsSync = request.SupportsSync,
- SupportsPersistentIdentifier = request.SupportsPersistentIdentifier
- });
- }
-
- public void Post(PostFullCapabilities request)
- {
- if (string.IsNullOrWhiteSpace(request.Id))
- {
- request.Id = GetSession(_sessionContext).Id;
- }
-
- _sessionManager.ReportCapabilities(request.Id, request);
- }
-
- public void Post(ReportViewing request)
- {
- request.SessionId = GetSession(_sessionContext).Id;
-
- _sessionManager.ReportNowViewingItem(request.SessionId, request.ItemId);
- }
- }
-}
diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs
deleted file mode 100644
index a70da8e56..000000000
--- a/MediaBrowser.Api/Subtitles/SubtitleService.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace MediaBrowser.Api.Subtitles
-{
- [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
- [Authenticated(Roles = "Admin")]
- public class DeleteSubtitle
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")]
- public int Index { get; set; }
- }
-
- [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
- [Authenticated]
- public class SearchRemoteSubtitles : IReturn<RemoteSubtitleInfo[]>
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Language { get; set; }
-
- public bool? IsPerfectMatch { get; set; }
- }
-
- [Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")]
- [Authenticated]
- public class DownloadRemoteSubtitles : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string SubtitleId { get; set; }
- }
-
- [Route("/Providers/Subtitles/Subtitles/{Id}", "GET")]
- [Authenticated]
- public class GetRemoteSubtitles : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
- [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
- public class GetSubtitle
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
- public int Index { get; set; }
-
- [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Format { get; set; }
-
- [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public long StartPositionTicks { get; set; }
-
- [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public long? EndPositionTicks { get; set; }
-
- [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool CopyTimestamps { get; set; }
-
- public bool AddVttTimeMap { get; set; }
- }
-
- [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
- [Authenticated]
- public class GetSubtitlePlaylist
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
- public int Index { get; set; }
-
- [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int SegmentLength { get; set; }
- }
-
- public class SubtitleService : BaseApiService
- {
- private readonly ILibraryManager _libraryManager;
- private readonly ISubtitleManager _subtitleManager;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
- private readonly IAuthorizationContext _authContext;
-
- public SubtitleService(
- ILogger<SubtitleService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ILibraryManager libraryManager,
- ISubtitleManager subtitleManager,
- ISubtitleEncoder subtitleEncoder,
- IMediaSourceManager mediaSourceManager,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _libraryManager = libraryManager;
- _subtitleManager = subtitleManager;
- _subtitleEncoder = subtitleEncoder;
- _mediaSourceManager = mediaSourceManager;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- _authContext = authContext;
- }
-
- public async Task<object> Get(GetSubtitlePlaylist request)
- {
- var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
-
- var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
-
- var builder = new StringBuilder();
-
- var runtime = mediaSource.RunTimeTicks ?? -1;
-
- if (runtime <= 0)
- {
- throw new ArgumentException("HLS Subtitles are not supported for this media.");
- }
-
- var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks;
- if (segmentLengthTicks <= 0)
- {
- throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
- }
-
- builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-VERSION:3");
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
-
- long positionTicks = 0;
-
- var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
-
- while (positionTicks < runtime)
- {
- var remaining = runtime - positionTicks;
- var lengthTicks = Math.Min(remaining, segmentLengthTicks);
-
- builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
-
- var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
-
- var url = string.Format("stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
- positionTicks.ToString(CultureInfo.InvariantCulture),
- endPositionTicks.ToString(CultureInfo.InvariantCulture),
- accessToken);
-
- builder.AppendLine(url);
-
- positionTicks += segmentLengthTicks;
- }
-
- builder.AppendLine("#EXT-X-ENDLIST");
-
- return ResultFactory.GetResult(Request, builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
- }
-
- public async Task<object> Get(GetSubtitle request)
- {
- if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase))
- {
- request.Format = "json";
- }
-
- if (string.IsNullOrEmpty(request.Format))
- {
- var item = (Video)_libraryManager.GetItemById(request.Id);
-
- var idString = request.Id.ToString("N", CultureInfo.InvariantCulture);
- var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false, null)
- .First(i => string.Equals(i.Id, request.MediaSourceId ?? idString));
-
- var subtitleStream = mediaSource.MediaStreams
- .First(i => i.Type == MediaStreamType.Subtitle && i.Index == request.Index);
-
- return await ResultFactory.GetStaticFileResult(Request, subtitleStream.Path).ConfigureAwait(false);
- }
-
- if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap)
- {
- using var stream = await GetSubtitles(request).ConfigureAwait(false);
- using var reader = new StreamReader(stream);
-
- var text = reader.ReadToEnd();
-
- text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000");
-
- return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format));
- }
-
- return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format));
- }
-
- private Task<Stream> GetSubtitles(GetSubtitle request)
- {
- var item = _libraryManager.GetItemById(request.Id);
-
- return _subtitleEncoder.GetSubtitles(item,
- request.MediaSourceId,
- request.Index,
- request.Format,
- request.StartPositionTicks,
- request.EndPositionTicks ?? 0,
- request.CopyTimestamps,
- CancellationToken.None);
- }
-
- public async Task<object> Get(SearchRemoteSubtitles request)
- {
- var video = (Video)_libraryManager.GetItemById(request.Id);
-
- return await _subtitleManager.SearchSubtitles(video, request.Language, request.IsPerfectMatch, CancellationToken.None).ConfigureAwait(false);
- }
-
- public Task Delete(DeleteSubtitle request)
- {
- var item = _libraryManager.GetItemById(request.Id);
- return _subtitleManager.DeleteSubtitles(item, request.Index);
- }
-
- public async Task<object> Get(GetRemoteSubtitles request)
- {
- var result = await _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).ConfigureAwait(false);
-
- return ResultFactory.GetResult(Request, result.Stream, MimeTypes.GetMimeType("file." + result.Format));
- }
-
- public void Post(DownloadRemoteSubtitles request)
- {
- var video = (Video)_libraryManager.GetItemById(request.Id);
-
- Task.Run(async () =>
- {
- try
- {
- await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None)
- .ConfigureAwait(false);
-
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error downloading subtitles");
- }
- });
- }
- }
-}
diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs
deleted file mode 100644
index 17afa8e79..000000000
--- a/MediaBrowser.Api/SuggestionsService.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Users/{UserId}/Suggestions", "GET", Summary = "Gets items based on a query.")]
- public class GetSuggestedItems : IReturn<QueryResult<BaseItemDto>>
- {
- public string MediaType { get; set; }
-
- public string Type { get; set; }
-
- public Guid UserId { get; set; }
-
- public bool EnableTotalRecordCount { get; set; }
-
- public int? StartIndex { get; set; }
-
- public int? Limit { get; set; }
-
- public string[] GetMediaTypes()
- {
- return (MediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetIncludeItemTypes()
- {
- return (Type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- public class SuggestionsService : BaseApiService
- {
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
-
- public SuggestionsService(
- ILogger<SuggestionsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IDtoService dtoService,
- IAuthorizationContext authContext,
- IUserManager userManager,
- ILibraryManager libraryManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _dtoService = dtoService;
- _authContext = authContext;
- _userManager = userManager;
- _libraryManager = libraryManager;
- }
-
- public object Get(GetSuggestedItems request)
- {
- return GetResultItems(request);
- }
-
- private QueryResult<BaseItemDto> GetResultItems(GetSuggestedItems request)
- {
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var dtoOptions = GetDtoOptions(_authContext, request);
- var result = GetItems(request, user, dtoOptions);
-
- var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
-
- return new QueryResult<BaseItemDto>
- {
- TotalRecordCount = result.TotalRecordCount,
- Items = dtoList
- };
- }
-
- private QueryResult<BaseItem> GetItems(GetSuggestedItems request, User user, DtoOptions dtoOptions)
- {
- return _libraryManager.GetItemsResult(new InternalItemsQuery(user)
- {
- OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
- MediaTypes = request.GetMediaTypes(),
- IncludeItemTypes = request.GetIncludeItemTypes(),
- IsVirtualItem = false,
- StartIndex = request.StartIndex,
- Limit = request.Limit,
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = request.EnableTotalRecordCount,
- Recursive = true
- });
- }
- }
-}
diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
deleted file mode 100644
index 88cddfff7..000000000
--- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
+++ /dev/null
@@ -1,279 +0,0 @@
-using System.Threading;
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.SyncPlay;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.SyncPlay;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.SyncPlay
-{
- [Route("/SyncPlay/NewGroup", "POST", Summary = "Create a new SyncPlay group")]
- [Authenticated]
- public class SyncPlayNewGroup : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")]
- [Authenticated]
- public class SyncPlayJoinGroup : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the Group id.
- /// </summary>
- /// <value>The Group id to join.</value>
- [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string GroupId { get; set; }
-
- /// <summary>
- /// Gets or sets the playing item id.
- /// </summary>
- /// <value>The client's currently playing item id.</value>
- [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string PlayingItemId { get; set; }
- }
-
- [Route("/SyncPlay/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")]
- [Authenticated]
- public class SyncPlayLeaveGroup : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/ListGroups", "GET", Summary = "List SyncPlay groups")]
- [Authenticated]
- public class SyncPlayListGroups : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the filter item id.
- /// </summary>
- /// <value>The filter item id.</value>
- [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string FilterItemId { get; set; }
- }
-
- [Route("/SyncPlay/PlayRequest", "POST", Summary = "Request play in SyncPlay group")]
- [Authenticated]
- public class SyncPlayPlayRequest : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")]
- [Authenticated]
- public class SyncPlayPauseRequest : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")]
- [Authenticated]
- public class SyncPlaySeekRequest : IReturnVoid
- {
- [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
- public long PositionTicks { get; set; }
- }
-
- [Route("/SyncPlay/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")]
- [Authenticated]
- public class SyncPlayBufferingRequest : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the date used to pin PositionTicks in time.
- /// </summary>
- /// <value>The date related to PositionTicks.</value>
- [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string When { get; set; }
-
- [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
- public long PositionTicks { get; set; }
-
- /// <summary>
- /// Gets or sets whether this is a buffering or a buffering-done request.
- /// </summary>
- /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
- [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
- public bool BufferingDone { get; set; }
- }
-
- [Route("/SyncPlay/UpdatePing", "POST", Summary = "Update session ping")]
- [Authenticated]
- public class SyncPlayUpdatePing : IReturnVoid
- {
- [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")]
- public double Ping { get; set; }
- }
-
- /// <summary>
- /// Class SyncPlayService.
- /// </summary>
- public class SyncPlayService : BaseApiService
- {
- /// <summary>
- /// The session context.
- /// </summary>
- private readonly ISessionContext _sessionContext;
-
- /// <summary>
- /// The SyncPlay manager.
- /// </summary>
- private readonly ISyncPlayManager _syncPlayManager;
-
- public SyncPlayService(
- ILogger<SyncPlayService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ISessionContext sessionContext,
- ISyncPlayManager syncPlayManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _sessionContext = sessionContext;
- _syncPlayManager = syncPlayManager;
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlayNewGroup request)
- {
- var currentSession = GetSession(_sessionContext);
- _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlayJoinGroup request)
- {
- var currentSession = GetSession(_sessionContext);
-
- Guid groupId;
- Guid playingItemId = Guid.Empty;
-
- if (!Guid.TryParse(request.GroupId, out groupId))
- {
- Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId);
- return;
- }
-
- // Both null and empty strings mean that client isn't playing anything
- if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId))
- {
- Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId);
- return;
- }
-
- var joinRequest = new JoinGroupRequest()
- {
- GroupId = groupId,
- PlayingItemId = playingItemId
- };
-
- _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlayLeaveGroup request)
- {
- var currentSession = GetSession(_sessionContext);
- _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <value>The requested list of groups.</value>
- public List<GroupInfoView> Post(SyncPlayListGroups request)
- {
- var currentSession = GetSession(_sessionContext);
- var filterItemId = Guid.Empty;
-
- if (!string.IsNullOrEmpty(request.FilterItemId) && !Guid.TryParse(request.FilterItemId, out filterItemId))
- {
- Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId);
- }
-
- return _syncPlayManager.ListGroups(currentSession, filterItemId);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlayPlayRequest request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Play
- };
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlayPauseRequest request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Pause
- };
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlaySeekRequest request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Seek,
- PositionTicks = request.PositionTicks
- };
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlayBufferingRequest request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
- When = DateTime.Parse(request.When),
- PositionTicks = request.PositionTicks
- };
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(SyncPlayUpdatePing request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.UpdatePing,
- Ping = Convert.ToInt64(request.Ping)
- };
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
- }
-}
diff --git a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs
deleted file mode 100644
index 4a9307e62..000000000
--- a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.SyncPlay;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.SyncPlay
-{
- [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")]
- public class GetUtcTime : IReturnVoid
- {
- // Nothing
- }
-
- /// <summary>
- /// Class TimeSyncService.
- /// </summary>
- public class TimeSyncService : BaseApiService
- {
- public TimeSyncService(
- ILogger<TimeSyncService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- // Do nothing
- }
-
- /// <summary>
- /// Handles the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <value>The current UTC time response.</value>
- public UtcTimeResponse Get(GetUtcTime request)
- {
- // Important to keep the following line at the beginning
- var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o");
-
- var response = new UtcTimeResponse();
- response.RequestReceptionTime = requestReceptionTime;
-
- // Important to keep the following two lines at the end
- var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o");
- response.ResponseTransmissionTime = responseTransmissionTime;
-
- // Implementing NTP on such a high level results in this useless
- // information being sent. On the other hand it enables future additions.
- return response;
- }
- }
-}
diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs
deleted file mode 100644
index 7ca31c21a..000000000
--- a/MediaBrowser.Api/System/ActivityLogService.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
- [Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")]
- public class GetActivityLogs : IReturn<QueryResult<ActivityLogEntry>>
- {
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "MinDate", Description = "Optional. The minimum date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string MinDate { get; set; }
-
- public bool? HasUserId { get; set; }
- }
-
- [Authenticated(Roles = "Admin")]
- public class ActivityLogService : BaseApiService
- {
- private readonly IActivityManager _activityManager;
-
- public ActivityLogService(
- ILogger<ActivityLogService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IActivityManager activityManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _activityManager = activityManager;
- }
-
- public object Get(GetActivityLogs request)
- {
- DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
- (DateTime?)null :
- DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-
- var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
- entries => entries.Where(entry => entry.DateCreated >= minDate));
-
- var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
-
- return ToOptimizedResult(result);
- }
- }
-}
diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs
deleted file mode 100644
index e0e20d828..000000000
--- a/MediaBrowser.Api/System/SystemService.cs
+++ /dev/null
@@ -1,221 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
- /// <summary>
- /// Class GetSystemInfo.
- /// </summary>
- [Route("/System/Info", "GET", Summary = "Gets information about the server")]
- [Authenticated(EscapeParentalControl = true, AllowBeforeStartupWizard = true)]
- public class GetSystemInfo : IReturn<SystemInfo>
- {
- }
-
- [Route("/System/Info/Public", "GET", Summary = "Gets public information about the server")]
- public class GetPublicSystemInfo : IReturn<PublicSystemInfo>
- {
- }
-
- [Route("/System/Ping", "POST")]
- [Route("/System/Ping", "GET")]
- public class PingSystem : IReturnVoid
- {
- }
-
- /// <summary>
- /// Class RestartApplication.
- /// </summary>
- [Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")]
- [Authenticated(Roles = "Admin", AllowLocal = true)]
- public class RestartApplication
- {
- }
-
- /// <summary>
- /// This is currently not authenticated because the uninstaller needs to be able to shutdown the server.
- /// </summary>
- [Route("/System/Shutdown", "POST", Summary = "Shuts down the application")]
- [Authenticated(Roles = "Admin", AllowLocal = true)]
- public class ShutdownApplication
- {
- }
-
- [Route("/System/Logs", "GET", Summary = "Gets a list of available server log files")]
- [Authenticated(Roles = "Admin")]
- public class GetServerLogs : IReturn<LogFile[]>
- {
- }
-
- [Route("/System/Endpoint", "GET", Summary = "Gets information about the request endpoint")]
- [Authenticated]
- public class GetEndpointInfo : IReturn<EndPointInfo>
- {
- public string Endpoint { get; set; }
- }
-
- [Route("/System/Logs/Log", "GET", Summary = "Gets a log file")]
- [Authenticated(Roles = "Admin")]
- public class GetLogFile
- {
- [ApiMember(Name = "Name", Description = "The log file name.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Name { get; set; }
- }
-
- [Route("/System/WakeOnLanInfo", "GET", Summary = "Gets wake on lan information")]
- [Authenticated]
- public class GetWakeOnLanInfo : IReturn<WakeOnLanInfo[]>
- {
- }
-
- /// <summary>
- /// Class SystemInfoService.
- /// </summary>
- public class SystemService : BaseApiService
- {
- /// <summary>
- /// The _app host.
- /// </summary>
- private readonly IServerApplicationHost _appHost;
- private readonly IApplicationPaths _appPaths;
- private readonly IFileSystem _fileSystem;
-
- private readonly INetworkManager _network;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SystemService" /> class.
- /// </summary>
- /// <param name="appHost">The app host.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <exception cref="ArgumentNullException">jsonSerializer</exception>
- public SystemService(
- ILogger<SystemService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IServerApplicationHost appHost,
- IFileSystem fileSystem,
- INetworkManager network)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _appHost = appHost;
- _fileSystem = fileSystem;
- _network = network;
- }
-
- public object Post(PingSystem request)
- {
- return _appHost.Name;
- }
-
- public object Get(GetWakeOnLanInfo request)
- {
- var result = _appHost.GetWakeOnLanInfo();
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetServerLogs request)
- {
- IEnumerable<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 ToOptimizedResult(result);
- }
-
- public Task<object> Get(GetLogFile request)
- {
- var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
- .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase));
-
- // For older files, assume fully static
- var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
-
- return ResultFactory.GetStaticFileResult(Request, file.FullName, fileShare);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public async Task<object> Get(GetSystemInfo request)
- {
- var result = await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Get(GetPublicSystemInfo request)
- {
- var result = await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(RestartApplication request)
- {
- _appHost.Restart();
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(ShutdownApplication request)
- {
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- await _appHost.Shutdown().ConfigureAwait(false);
- });
- }
-
- public object Get(GetEndpointInfo request)
- {
- return ToOptimizedResult(new EndPointInfo
- {
- IsLocal = Request.IsLocal,
- IsInNetwork = _network.IsInLocalNetwork(request.Endpoint ?? Request.RemoteIp)
- });
- }
- }
-}
diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs
deleted file mode 100644
index 165abd613..000000000
--- a/MediaBrowser.Api/TvShowsService.cs
+++ /dev/null
@@ -1,497 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.TV;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class GetNextUpEpisodes.
- /// </summary>
- [Route("/Shows/NextUp", "GET", Summary = "Gets a list of next up episodes")]
- public class GetNextUpEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "SeriesId", Description = "Optional. Filter by series id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SeriesId { get; set; }
-
- /// <summary>
- /// Specify this to localize the search to a specific item or folder. Omit to use the root.
- /// </summary>
- /// <value>The parent id.</value>
- [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ParentId { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- public bool EnableTotalRecordCount { get; set; }
-
- public GetNextUpEpisodes()
- {
- EnableTotalRecordCount = true;
- }
- }
-
- [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")]
- public class GetUpcomingEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- /// <summary>
- /// Specify this to localize the search to a specific item or folder. Omit to use the root.
- /// </summary>
- /// <value>The parent id.</value>
- [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ParentId { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
- }
-
- [Route("/Shows/{Id}/Episodes", "GET", Summary = "Gets episodes for a tv season")]
- public class GetEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasItemFields, IHasDtoOptions
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "Season", Description = "Optional filter by season number.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public int? Season { get; set; }
-
- [ApiMember(Name = "SeasonId", Description = "Optional. Filter by season id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SeasonId { get; set; }
-
- [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsMissing { get; set; }
-
- [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string AdjacentTo { get; set; }
-
- [ApiMember(Name = "StartItemId", Description = "Optional. Skip through the list until a given item is found.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string StartItemId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string SortBy { get; set; }
-
- [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public SortOrder? SortOrder { get; set; }
- }
-
- [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")]
- public class GetSeasons : IReturn<QueryResult<BaseItemDto>>, IHasItemFields, IHasDtoOptions
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Id { get; set; }
-
- [ApiMember(Name = "IsSpecialSeason", Description = "Optional. Filter by special season.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsSpecialSeason { get; set; }
-
- [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsMissing { get; set; }
-
- [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string AdjacentTo { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
- }
-
- /// <summary>
- /// Class TvShowsService.
- /// </summary>
- [Authenticated]
- public class TvShowsService : BaseApiService
- {
- /// <summary>
- /// The _user manager.
- /// </summary>
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- private readonly IDtoService _dtoService;
- private readonly ITVSeriesManager _tvSeriesManager;
- private readonly IAuthorizationContext _authContext;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TvShowsService" /> class.
- /// </summary>
- /// <param name="userManager">The user manager.</param>
- /// <param name="userDataManager">The user data repository.</param>
- /// <param name="libraryManager">The library manager.</param>
- public TvShowsService(
- ILogger<TvShowsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- ITVSeriesManager tvSeriesManager,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _tvSeriesManager = tvSeriesManager;
- _authContext = authContext;
- }
-
- public object Get(GetUpcomingEpisodes request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
-
- var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId);
-
- var options = GetDtoOptions(_authContext, request);
-
- var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { typeof(Episode).Name },
- OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
- MinPremiereDate = minPremiereDate,
- StartIndex = request.StartIndex,
- Limit = request.Limit,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = options
- });
-
- var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
-
- var result = new QueryResult<BaseItemDto>
- {
- TotalRecordCount = itemsResult.Count,
- Items = returnItems
- };
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetNextUpEpisodes request)
- {
- var options = GetDtoOptions(_authContext, request);
-
- var result = _tvSeriesManager.GetNextUp(new NextUpQuery
- {
- Limit = request.Limit,
- ParentId = request.ParentId,
- SeriesId = request.SeriesId,
- StartIndex = request.StartIndex,
- UserId = request.UserId,
- EnableTotalRecordCount = request.EnableTotalRecordCount
- }, options);
-
- var user = _userManager.GetUserById(request.UserId);
-
- var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
-
- return ToOptimizedResult(new QueryResult<BaseItemDto>
- {
- TotalRecordCount = result.TotalRecordCount,
- Items = 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;
- }
-
- public object Get(GetSeasons request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var series = GetSeries(request.Id);
-
- if (series == null)
- {
- throw new ResourceNotFoundException("Series not found");
- }
-
- var seasons = series.GetItemList(new InternalItemsQuery(user)
- {
- IsMissing = request.IsMissing,
- IsSpecialSeason = request.IsSpecialSeason,
- AdjacentTo = request.AdjacentTo
- });
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
-
- return new QueryResult<BaseItemDto>
- {
- TotalRecordCount = returnItems.Count,
- Items = returnItems
- };
- }
-
- private Series GetSeries(string seriesId)
- {
- if (!string.IsNullOrWhiteSpace(seriesId))
- {
- return _libraryManager.GetItemById(seriesId) as Series;
- }
-
- return null;
- }
-
- public object Get(GetEpisodes request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- List<BaseItem> episodes;
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- if (!string.IsNullOrWhiteSpace(request.SeasonId))
- {
- if (!(_libraryManager.GetItemById(new Guid(request.SeasonId)) is Season season))
- {
- throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId);
- }
-
- episodes = season.GetEpisodes(user, dtoOptions);
- }
- else if (request.Season.HasValue)
- {
- var series = GetSeries(request.Id);
-
- if (series == null)
- {
- throw new ResourceNotFoundException("Series not found");
- }
-
- var season = series.GetSeasons(user, dtoOptions).FirstOrDefault(i => i.IndexNumber == request.Season.Value);
-
- episodes = season == null ? new List<BaseItem>() : ((Season)season).GetEpisodes(user, dtoOptions);
- }
- else
- {
- var series = GetSeries(request.Id);
-
- if (series == null)
- {
- throw new ResourceNotFoundException("Series not found");
- }
-
- episodes = series.GetEpisodes(user, dtoOptions).ToList();
- }
-
- // Filter after the fact in case the ui doesn't want them
- if (request.IsMissing.HasValue)
- {
- var val = request.IsMissing.Value;
- episodes = episodes.Where(i => ((Episode)i).IsMissingEpisode == val).ToList();
- }
-
- if (!string.IsNullOrWhiteSpace(request.StartItemId))
- {
- episodes = episodes.SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), request.StartItemId, StringComparison.OrdinalIgnoreCase)).ToList();
- }
-
- // This must be the last filter
- if (!string.IsNullOrEmpty(request.AdjacentTo))
- {
- episodes = UserViewBuilder.FilterForAdjacency(episodes, request.AdjacentTo).ToList();
- }
-
- if (string.Equals(request.SortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
- {
- episodes.Shuffle();
- }
-
- var returnItems = episodes;
-
- if (request.StartIndex.HasValue || request.Limit.HasValue)
- {
- returnItems = ApplyPaging(episodes, request.StartIndex, request.Limit).ToList();
- }
-
- var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
-
- return new QueryResult<BaseItemDto>
- {
- TotalRecordCount = episodes.Count,
- Items = dtos
- };
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs
deleted file mode 100644
index 9875e0208..000000000
--- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class GetArtists.
- /// </summary>
- [Route("/Artists", "GET", Summary = "Gets all artists from a given item, folder, or the entire library")]
- public class GetArtists : GetItemsByName
- {
- }
-
- [Route("/Artists/AlbumArtists", "GET", Summary = "Gets all album artists from a given item, folder, or the entire library")]
- public class GetAlbumArtists : GetItemsByName
- {
- }
-
- [Route("/Artists/{Name}", "GET", Summary = "Gets an artist, by name")]
- public class GetArtist : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- /// <summary>
- /// Class ArtistsService.
- /// </summary>
- [Authenticated]
- public class ArtistsService : BaseItemsByNameService<MusicArtist>
- {
- public ArtistsService(
- ILogger<ArtistsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IAuthorizationContext authorizationContext)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- userDataRepository,
- dtoService,
- authorizationContext)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetArtist request)
- {
- return GetItem(request);
- }
-
- /// <summary>
- /// Gets the item.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{BaseItemDto}.</returns>
- private BaseItemDto GetItem(GetArtist request)
- {
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- var item = GetArtist(request.Name, LibraryManager, dtoOptions);
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- var user = UserManager.GetUserById(request.UserId);
-
- return DtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- return DtoService.GetBaseItemDto(item, dtoOptions);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetArtists request)
- {
- return GetResultSlim(request);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetAlbumArtists request)
- {
- var result = GetResultSlim(request);
-
- return ToOptimizedResult(result);
- }
-
- protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
- {
- return request is GetAlbumArtists ? LibraryManager.GetAlbumArtists(query) : LibraryManager.GetArtists(query);
- }
-
- /// <summary>
- /// Gets all items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="items">The items.</param>
- /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
- protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
deleted file mode 100644
index fd639caf1..000000000
--- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
+++ /dev/null
@@ -1,388 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class BaseItemsByNameService.
- /// </summary>
- /// <typeparam name="TItemType">The type of the T item type.</typeparam>
- public abstract class BaseItemsByNameService<TItemType> : BaseApiService
- where TItemType : BaseItem, IItemByName
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseItemsByNameService{TItemType}" /> class.
- /// </summary>
- /// <param name="userManager">The user manager.</param>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="userDataRepository">The user data repository.</param>
- /// <param name="dtoService">The dto service.</param>
- protected BaseItemsByNameService(
- ILogger<BaseItemsByNameService<TItemType>> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IAuthorizationContext authorizationContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- UserManager = userManager;
- LibraryManager = libraryManager;
- UserDataRepository = userDataRepository;
- DtoService = dtoService;
- AuthorizationContext = authorizationContext;
- }
-
- /// <summary>
- /// Gets the _user manager.
- /// </summary>
- protected IUserManager UserManager { get; }
-
- /// <summary>
- /// Gets the library manager.
- /// </summary>
- protected ILibraryManager LibraryManager { get; }
-
- protected IUserDataManager UserDataRepository { get; }
-
- protected IDtoService DtoService { get; }
-
- protected IAuthorizationContext AuthorizationContext { get; }
-
- protected BaseItem GetParentItem(GetItemsByName request)
- {
- BaseItem parentItem;
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- var user = UserManager.GetUserById(request.UserId);
- parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
- }
-
- return parentItem;
- }
-
- protected string GetParentItemViewType(GetItemsByName request)
- {
- var parent = GetParentItem(request);
-
- if (parent is IHasCollectionType collectionFolder)
- {
- return collectionFolder.CollectionType;
- }
-
- return null;
- }
-
- protected QueryResult<BaseItemDto> GetResultSlim(GetItemsByName request)
- {
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- User user = null;
- BaseItem parentItem;
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- user = UserManager.GetUserById(request.UserId);
- parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
- }
-
- var excludeItemTypes = request.GetExcludeItemTypes();
- var includeItemTypes = request.GetIncludeItemTypes();
- var mediaTypes = request.GetMediaTypes();
-
- var query = new InternalItemsQuery(user)
- {
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- StartIndex = request.StartIndex,
- Limit = request.Limit,
- IsFavorite = request.IsFavorite,
- NameLessThan = request.NameLessThan,
- NameStartsWith = request.NameStartsWith,
- NameStartsWithOrGreater = request.NameStartsWithOrGreater,
- Tags = request.GetTags(),
- OfficialRatings = request.GetOfficialRatings(),
- Genres = request.GetGenres(),
- GenreIds = GetGuids(request.GenreIds),
- StudioIds = GetGuids(request.StudioIds),
- Person = request.Person,
- PersonIds = GetGuids(request.PersonIds),
- PersonTypes = request.GetPersonTypes(),
- Years = request.GetYears(),
- MinCommunityRating = request.MinCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = request.SearchTerm,
- EnableTotalRecordCount = request.EnableTotalRecordCount
- };
-
- if (!string.IsNullOrWhiteSpace(request.ParentId))
- {
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { new Guid(request.ParentId) };
- }
- else
- {
- query.ItemIds = new[] { new Guid(request.ParentId) };
- }
- }
-
- // Studios
- if (!string.IsNullOrEmpty(request.Studios))
- {
- query.StudioIds = request.Studios.Split('|').Select(i =>
- {
- try
- {
- return LibraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null).Select(i => i.Id).ToArray();
- }
-
- foreach (var filter in request.GetFilters())
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- var result = GetItems(request, query);
-
- var dtos = result.Items.Select(i =>
- {
- var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(request.IncludeItemTypes))
- {
- SetItemCounts(dto, i.Item2);
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
- }
-
- protected virtual QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
- {
- return new QueryResult<(BaseItem, ItemCounts)>();
- }
-
- private void SetItemCounts(BaseItemDto dto, ItemCounts counts)
- {
- 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;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{ItemsResult}.</returns>
- protected QueryResult<BaseItemDto> GetResult(GetItemsByName request)
- {
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- User user = null;
- BaseItem parentItem;
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- user = UserManager.GetUserById(request.UserId);
- parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
- }
-
- IList<BaseItem> items;
-
- var excludeItemTypes = request.GetExcludeItemTypes();
- var includeItemTypes = request.GetIncludeItemTypes();
- var mediaTypes = request.GetMediaTypes();
-
- var query = new InternalItemsQuery(user)
- {
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- DtoOptions = dtoOptions
- };
-
- bool Filter(BaseItem i) => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes);
-
- if (parentItem.IsFolder)
- {
- var folder = (Folder)parentItem;
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- items = request.Recursive ?
- folder.GetRecursiveChildren(user, query).ToList() :
- folder.GetChildren(user, true).Where(Filter).ToList();
- }
- else
- {
- items = request.Recursive ?
- folder.GetRecursiveChildren(Filter) :
- folder.Children.Where(Filter).ToList();
- }
- }
- else
- {
- items = new[] { parentItem }.Where(Filter).ToList();
- }
-
- var extractedItems = GetAllItems(request, items);
-
- var filteredItems = LibraryManager.Sort(extractedItems, user, request.GetOrderBy());
-
- var ibnItemsArray = filteredItems.ToList();
-
- IEnumerable<BaseItem> ibnItems = ibnItemsArray;
-
- var result = new QueryResult<BaseItemDto>
- {
- TotalRecordCount = ibnItemsArray.Count
- };
-
- if (request.StartIndex.HasValue || request.Limit.HasValue)
- {
- if (request.StartIndex.HasValue)
- {
- ibnItems = ibnItems.Skip(request.StartIndex.Value);
- }
-
- if (request.Limit.HasValue)
- {
- ibnItems = ibnItems.Take(request.Limit.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));
-
- result.Items = dtos.Where(i => i != null).ToArray();
-
- return result;
- }
-
- /// <summary>
- /// Filters the items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="f">The f.</param>
- /// <param name="excludeItemTypes">The exclude item types.</param>
- /// <param name="includeItemTypes">The include item types.</param>
- /// <param name="mediaTypes">The media types.</param>
- /// <returns>IEnumerable{BaseItem}.</returns>
- private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes)
- {
- // Exclude item types
- if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
-
- // Include item types
- if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
-
- // Include MediaTypes
- if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
-
- return true;
- }
-
- /// <summary>
- /// Gets all items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="items">The items.</param>
- /// <returns>IEnumerable{Task{`0}}.</returns>
- protected abstract IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items);
- }
-
- /// <summary>
- /// Class GetItemsByName.
- /// </summary>
- public class GetItemsByName : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
- {
- public GetItemsByName()
- {
- Recursive = true;
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
deleted file mode 100644
index 344861a49..000000000
--- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
+++ /dev/null
@@ -1,478 +0,0 @@
-using System;
-using System.Linq;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- public abstract class BaseItemsRequest : IHasDtoOptions
- {
- protected BaseItemsRequest()
- {
- EnableImages = true;
- EnableTotalRecordCount = true;
- }
-
- /// <summary>
- /// Gets or sets the max offical rating.
- /// </summary>
- /// <value>The max offical rating.</value>
- [ApiMember(Name = "MaxOfficialRating", Description = "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string MaxOfficialRating { get; set; }
-
- [ApiMember(Name = "HasThemeSong", Description = "Optional filter by items with theme songs.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? HasThemeSong { get; set; }
-
- [ApiMember(Name = "HasThemeVideo", Description = "Optional filter by items with theme videos.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? HasThemeVideo { get; set; }
-
- [ApiMember(Name = "HasSubtitles", Description = "Optional filter by items with subtitles.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? HasSubtitles { get; set; }
-
- [ApiMember(Name = "HasSpecialFeature", Description = "Optional filter by items with special features.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? HasSpecialFeature { get; set; }
-
- [ApiMember(Name = "HasTrailer", Description = "Optional filter by items with trailers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? HasTrailer { get; set; }
-
- [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string AdjacentTo { get; set; }
-
- [ApiMember(Name = "MinIndexNumber", Description = "Optional filter by minimum index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MinIndexNumber { get; set; }
-
- [ApiMember(Name = "ParentIndexNumber", Description = "Optional filter by parent index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ParentIndexNumber { get; set; }
-
- [ApiMember(Name = "HasParentalRating", Description = "Optional filter by items that have or do not have a parental rating", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? HasParentalRating { get; set; }
-
- [ApiMember(Name = "IsHD", Description = "Optional filter by items that are HD or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsHD { get; set; }
-
- public bool? Is4K { get; set; }
-
- [ApiMember(Name = "LocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string LocationTypes { get; set; }
-
- [ApiMember(Name = "ExcludeLocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ExcludeLocationTypes { get; set; }
-
- [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsMissing { get; set; }
-
- [ApiMember(Name = "IsUnaired", Description = "Optional filter by items that are unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsUnaired { get; set; }
-
- [ApiMember(Name = "MinCommunityRating", Description = "Optional filter by minimum community rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public double? MinCommunityRating { get; set; }
-
- [ApiMember(Name = "MinCriticRating", Description = "Optional filter by minimum critic rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public double? MinCriticRating { get; set; }
-
- [ApiMember(Name = "AiredDuringSeason", Description = "Gets all episodes that aired during a season, including specials.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? AiredDuringSeason { get; set; }
-
- [ApiMember(Name = "MinPremiereDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string MinPremiereDate { get; set; }
-
- [ApiMember(Name = "MinDateLastSaved", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string MinDateLastSaved { get; set; }
-
- [ApiMember(Name = "MinDateLastSavedForUser", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string MinDateLastSavedForUser { get; set; }
-
- [ApiMember(Name = "MaxPremiereDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string MaxPremiereDate { get; set; }
-
- [ApiMember(Name = "HasOverview", Description = "Optional filter by items that have an overview or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? HasOverview { get; set; }
-
- [ApiMember(Name = "HasImdbId", Description = "Optional filter by items that have an imdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? HasImdbId { get; set; }
-
- [ApiMember(Name = "HasTmdbId", Description = "Optional filter by items that have a tmdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? HasTmdbId { get; set; }
-
- [ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? HasTvdbId { get; set; }
-
- [ApiMember(Name = "ExcludeItemIds", Description = "Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ExcludeItemIds { get; set; }
-
- public bool EnableTotalRecordCount { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Limit { get; set; }
-
- /// <summary>
- /// Whether or not to perform the query recursively.
- /// </summary>
- /// <value><c>true</c> if recursive; otherwise, <c>false</c>.</value>
- [ApiMember(Name = "Recursive", Description = "When searching within folders, this determines whether or not the search will be recursive. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool Recursive { get; set; }
-
- public string SearchTerm { get; set; }
-
- /// <summary>
- /// Gets or sets the sort order.
- /// </summary>
- /// <value>The sort order.</value>
- [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string SortOrder { get; set; }
-
- /// <summary>
- /// Specify this to localize the search to a specific item or folder. Omit to use the root.
- /// </summary>
- /// <value>The parent id.</value>
- [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string ParentId { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- /// <summary>
- /// Gets or sets the exclude item types.
- /// </summary>
- /// <value>The exclude item types.</value>
- [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ExcludeItemTypes { get; set; }
-
- /// <summary>
- /// Gets or sets the include item types.
- /// </summary>
- /// <value>The include item types.</value>
- [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string IncludeItemTypes { get; set; }
-
- /// <summary>
- /// Filters to apply to the results.
- /// </summary>
- /// <value>The filters.</value>
- [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Filters { get; set; }
-
- /// <summary>
- /// Gets or sets the Isfavorite option.
- /// </summary>
- /// <value>IsFavorite</value>
- [ApiMember(Name = "IsFavorite", Description = "Optional filter by items that are marked as favorite, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsFavorite { get; set; }
-
- /// <summary>
- /// Gets or sets the media types.
- /// </summary>
- /// <value>The media types.</value>
- [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string MediaTypes { get; set; }
-
- /// <summary>
- /// Gets or sets the image types.
- /// </summary>
- /// <value>The image types.</value>
- [ApiMember(Name = "ImageTypes", Description = "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ImageTypes { get; set; }
-
- /// <summary>
- /// What to sort the results by.
- /// </summary>
- /// <value>The sort by.</value>
- [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string SortBy { get; set; }
-
- [ApiMember(Name = "IsPlayed", Description = "Optional filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsPlayed { get; set; }
-
- /// <summary>
- /// Limit results to items containing specific genres.
- /// </summary>
- /// <value>The genres.</value>
- [ApiMember(Name = "Genres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Genres { get; set; }
-
- public string GenreIds { get; set; }
-
- [ApiMember(Name = "OfficialRatings", Description = "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string OfficialRatings { get; set; }
-
- [ApiMember(Name = "Tags", Description = "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Tags { get; set; }
-
- /// <summary>
- /// Limit results to items containing specific years.
- /// </summary>
- /// <value>The years.</value>
- [ApiMember(Name = "Years", Description = "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Years { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- /// <summary>
- /// Limit results to items containing a specific person.
- /// </summary>
- /// <value>The person.</value>
- [ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Person { get; set; }
-
- [ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string PersonIds { get; set; }
-
- /// <summary>
- /// If the Person filter is used, this can also be used to restrict to a specific person type.
- /// </summary>
- /// <value>The type of the person.</value>
- [ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string PersonTypes { get; set; }
-
- /// <summary>
- /// Limit results to items containing specific studios.
- /// </summary>
- /// <value>The studios.</value>
- [ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Studios { get; set; }
-
- [ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string StudioIds { get; set; }
-
- /// <summary>
- /// Gets or sets the studios.
- /// </summary>
- /// <value>The studios.</value>
- [ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Artists { get; set; }
-
- public string ExcludeArtistIds { get; set; }
-
- [ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string ArtistIds { get; set; }
-
- public string AlbumArtistIds { get; set; }
-
- public string ContributingArtistIds { get; set; }
-
- [ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Albums { get; set; }
-
- public string AlbumIds { get; set; }
-
- /// <summary>
- /// Gets or sets the item ids.
- /// </summary>
- /// <value>The item ids.</value>
- [ApiMember(Name = "Ids", Description = "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Ids { get; set; }
-
- /// <summary>
- /// Gets or sets the video types.
- /// </summary>
- /// <value>The video types.</value>
- [ApiMember(Name = "VideoTypes", Description = "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string VideoTypes { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the min offical rating.
- /// </summary>
- /// <value>The min offical rating.</value>
- [ApiMember(Name = "MinOfficialRating", Description = "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string MinOfficialRating { get; set; }
-
- [ApiMember(Name = "IsLocked", Description = "Optional filter by items that are locked.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? IsLocked { get; set; }
-
- [ApiMember(Name = "IsPlaceHolder", Description = "Optional filter by items that are placeholders", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? IsPlaceHolder { get; set; }
-
- [ApiMember(Name = "HasOfficialRating", Description = "Optional filter by items that have official ratings", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public bool? HasOfficialRating { get; set; }
-
- [ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? CollapseBoxSetItems { get; set; }
-
- public int? MinWidth { get; set; }
-
- public int? MinHeight { get; set; }
-
- public int? MaxWidth { get; set; }
-
- public int? MaxHeight { get; set; }
-
- /// <summary>
- /// Gets or sets the video formats.
- /// </summary>
- /// <value>The video formats.</value>
- [ApiMember(Name = "Is3D", Description = "Optional filter by items that are 3D, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? Is3D { get; set; }
-
- /// <summary>
- /// Gets or sets the series status.
- /// </summary>
- /// <value>The series status.</value>
- [ApiMember(Name = "SeriesStatus", Description = "Optional filter by Series Status. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string SeriesStatus { get; set; }
-
- [ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string NameStartsWithOrGreater { get; set; }
-
- [ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string NameStartsWith { get; set; }
-
- [ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is equally or lesser than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string NameLessThan { get; set; }
-
- public string[] GetGenres()
- {
- return (Genres ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetTags()
- {
- return (Tags ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetOfficialRatings()
- {
- return (OfficialRatings ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetMediaTypes()
- {
- return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetIncludeItemTypes()
- {
- return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetExcludeItemTypes()
- {
- return (ExcludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public int[] GetYears()
- {
- return (Years ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
- }
-
- public string[] GetStudios()
- {
- return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public string[] GetPersonTypes()
- {
- return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public VideoType[] GetVideoTypes()
- {
- return string.IsNullOrEmpty(VideoTypes)
- ? Array.Empty<VideoType>()
- : VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(v => Enum.Parse<VideoType>(v, true)).ToArray();
- }
-
- /// <summary>
- /// Gets the filters.
- /// </summary>
- /// <returns>IEnumerable{ItemFilter}.</returns>
- public ItemFilter[] GetFilters()
- {
- var val = Filters;
-
- return string.IsNullOrEmpty(val)
- ? Array.Empty<ItemFilter>()
- : val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
- }
-
- /// <summary>
- /// Gets the image types.
- /// </summary>
- /// <returns>IEnumerable{ImageType}.</returns>
- public ImageType[] GetImageTypes()
- {
- var val = ImageTypes;
-
- return string.IsNullOrEmpty(val)
- ? Array.Empty<ImageType>()
- : val.Split(',').Select(v => Enum.Parse<ImageType>(v, true)).ToArray();
- }
-
- /// <summary>
- /// Gets the order by.
- /// </summary>
- /// <returns>IEnumerable{ItemSortBy}.</returns>
- public ValueTuple<string, SortOrder>[] GetOrderBy()
- {
- return GetOrderBy(SortBy, SortOrder);
- }
-
- public static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder)
- {
- var val = sortBy;
-
- if (string.IsNullOrEmpty(val))
- {
- return Array.Empty<ValueTuple<string, SortOrder>>();
- }
-
- var vals = val.Split(',');
- if (string.IsNullOrWhiteSpace(requestedSortOrder))
- {
- requestedSortOrder = "Ascending";
- }
-
- var sortOrders = requestedSortOrder.Split(',');
-
- var result = new ValueTuple<string, SortOrder>[vals.Length];
-
- for (var i = 0; i < vals.Length; i++)
- {
- var sortOrderIndex = sortOrders.Length > i ? i : 0;
-
- var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
- var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
- ? MediaBrowser.Model.Entities.SortOrder.Descending
- : MediaBrowser.Model.Entities.SortOrder.Ascending;
-
- result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
- }
-
- return result;
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs
deleted file mode 100644
index 7bdfbac98..000000000
--- a/MediaBrowser.Api/UserLibrary/GenresService.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class GetGenres.
- /// </summary>
- [Route("/Genres", "GET", Summary = "Gets all genres from a given item, folder, or the entire library")]
- public class GetGenres : GetItemsByName
- {
- }
-
- /// <summary>
- /// Class GetGenre.
- /// </summary>
- [Route("/Genres/{Name}", "GET", Summary = "Gets a genre, by name")]
- public class GetGenre : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- /// <summary>
- /// Class GenresService.
- /// </summary>
- [Authenticated]
- public class GenresService : BaseItemsByNameService<Genre>
- {
- public GenresService(
- ILogger<GenresService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IAuthorizationContext authorizationContext)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- userDataRepository,
- dtoService,
- authorizationContext)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetGenre request)
- {
- var result = GetItem(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the item.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{BaseItemDto}.</returns>
- private BaseItemDto GetItem(GetGenre request)
- {
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- var item = GetGenre(request.Name, LibraryManager, dtoOptions);
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- var user = UserManager.GetUserById(request.UserId);
-
- return DtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- return DtoService.GetBaseItemDto(item, dtoOptions);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetGenres request)
- {
- var result = GetResultSlim(request);
-
- return ToOptimizedResult(result);
- }
-
- protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
- {
- var viewType = GetParentItemViewType(request);
-
- if (string.Equals(viewType, CollectionType.Music) || string.Equals(viewType, CollectionType.MusicVideos))
- {
- return LibraryManager.GetMusicGenres(query);
- }
-
- return LibraryManager.GetGenres(query);
- }
-
- /// <summary>
- /// Gets all items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="items">The items.</param>
- /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
- protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
deleted file mode 100644
index 7efe0552c..000000000
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ /dev/null
@@ -1,514 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class GetItems.
- /// </summary>
- [Route("/Items", "GET", Summary = "Gets items based on a query.")]
- [Route("/Users/{UserId}/Items", "GET", Summary = "Gets items based on a query.")]
- public class GetItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
- {
- }
-
- [Route("/Users/{UserId}/Items/Resume", "GET", Summary = "Gets items based on a query.")]
- public class GetResumeItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
- {
- }
-
- /// <summary>
- /// Class ItemsService.
- /// </summary>
- [Authenticated]
- public class ItemsService : BaseApiService
- {
- /// <summary>
- /// The _user manager.
- /// </summary>
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
-
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemsService" /> class.
- /// </summary>
- /// <param name="userManager">The user manager.</param>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="localization">The localization.</param>
- /// <param name="dtoService">The dto service.</param>
- public ItemsService(
- ILogger<ItemsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- IDtoService dtoService,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _localization = localization;
- _dtoService = dtoService;
- _authContext = authContext;
- }
-
- public object Get(GetResumeItems request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId);
-
- var options = GetDtoOptions(_authContext, request);
-
- var ancestorIds = Array.Empty<Guid>();
-
- var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
- if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
- {
- ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
- .Where(i => i is Folder)
- .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
- .Select(i => i.Id)
- .ToArray();
- }
-
- var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
- {
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
- IsResumable = true,
- StartIndex = request.StartIndex,
- Limit = request.Limit,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = options,
- MediaTypes = request.GetMediaTypes(),
- IsVirtualItem = false,
- CollapseBoxSetItems = false,
- EnableTotalRecordCount = request.EnableTotalRecordCount,
- AncestorIds = ancestorIds,
- IncludeItemTypes = request.GetIncludeItemTypes(),
- ExcludeItemTypes = request.GetExcludeItemTypes(),
- SearchTerm = request.SearchTerm
- });
-
- var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, options, user);
-
- var result = new QueryResult<BaseItemDto>
- {
- StartIndex = request.StartIndex.GetValueOrDefault(),
- TotalRecordCount = itemsResult.TotalRecordCount,
- Items = returnItems
- };
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetItems request)
- {
- if (request == null)
- {
- throw new ArgumentNullException(nameof(request));
- }
-
- var result = GetItems(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the items.
- /// </summary>
- /// <param name="request">The request.</param>
- private QueryResult<BaseItemDto> GetItems(GetItems request)
- {
- var user = request.UserId == Guid.Empty ? null : _userManager.GetUserById(request.UserId);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = GetQueryResult(request, dtoOptions, user);
-
- if (result == null)
- {
- throw new InvalidOperationException("GetItemsToSerialize returned null");
- }
-
- if (result.Items == null)
- {
- throw new InvalidOperationException("GetItemsToSerialize result.Items returned null");
- }
-
- var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
-
- return new QueryResult<BaseItemDto>
- {
- StartIndex = request.StartIndex.GetValueOrDefault(),
- TotalRecordCount = result.TotalRecordCount,
- Items = dtoList
- };
- }
-
- /// <summary>
- /// Gets the items to serialize.
- /// </summary>
- private QueryResult<BaseItem> GetQueryResult(GetItems request, DtoOptions dtoOptions, User user)
- {
- if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
- || string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
- {
- request.ParentId = null;
- }
-
- BaseItem item = null;
-
- if (!string.IsNullOrEmpty(request.ParentId))
- {
- item = _libraryManager.GetItemById(request.ParentId);
- }
-
- if (item == null)
- {
- item = _libraryManager.GetUserRootFolder();
- }
-
- if (!(item is Folder folder))
- {
- folder = _libraryManager.GetUserRootFolder();
- }
-
- if (folder is IHasCollectionType hasCollectionType
- && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
- {
- request.Recursive = true;
- request.IncludeItemTypes = "Playlist";
- }
-
- bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
- // Assume all folders inside an EnabledChannel are enabled
- || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
-
- var collectionFolders = _libraryManager.GetCollectionFolders(item);
- foreach (var collectionFolder in collectionFolders)
- {
- if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
- collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
- StringComparer.OrdinalIgnoreCase))
- {
- isInEnabledFolder = true;
- }
- }
-
- if (!(item is UserRootFolder)
- && !isInEnabledFolder
- && !user.HasPermission(PermissionKind.EnableAllFolders)
- && !user.HasPermission(PermissionKind.EnableAllChannels))
- {
- Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
- return new QueryResult<BaseItem>
- {
- Items = Array.Empty<BaseItem>(),
- TotalRecordCount = 0,
- StartIndex = 0
- };
- }
-
- if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder))
- {
- return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
- }
-
- var itemsArray = folder.GetChildren(user, true);
- return new QueryResult<BaseItem>
- {
- Items = itemsArray,
- TotalRecordCount = itemsArray.Count,
- StartIndex = 0
- };
- }
-
- private InternalItemsQuery GetItemsQuery(GetItems request, DtoOptions dtoOptions, User user)
- {
- var query = new InternalItemsQuery(user)
- {
- IsPlayed = request.IsPlayed,
- MediaTypes = request.GetMediaTypes(),
- IncludeItemTypes = request.GetIncludeItemTypes(),
- ExcludeItemTypes = request.GetExcludeItemTypes(),
- Recursive = request.Recursive,
- OrderBy = request.GetOrderBy(),
-
- IsFavorite = request.IsFavorite,
- Limit = request.Limit,
- StartIndex = request.StartIndex,
- IsMissing = request.IsMissing,
- IsUnaired = request.IsUnaired,
- CollapseBoxSetItems = request.CollapseBoxSetItems,
- NameLessThan = request.NameLessThan,
- NameStartsWith = request.NameStartsWith,
- NameStartsWithOrGreater = request.NameStartsWithOrGreater,
- HasImdbId = request.HasImdbId,
- IsPlaceHolder = request.IsPlaceHolder,
- IsLocked = request.IsLocked,
- MinWidth = request.MinWidth,
- MinHeight = request.MinHeight,
- MaxWidth = request.MaxWidth,
- MaxHeight = request.MaxHeight,
- Is3D = request.Is3D,
- HasTvdbId = request.HasTvdbId,
- HasTmdbId = request.HasTmdbId,
- HasOverview = request.HasOverview,
- HasOfficialRating = request.HasOfficialRating,
- HasParentalRating = request.HasParentalRating,
- HasSpecialFeature = request.HasSpecialFeature,
- HasSubtitles = request.HasSubtitles,
- HasThemeSong = request.HasThemeSong,
- HasThemeVideo = request.HasThemeVideo,
- HasTrailer = request.HasTrailer,
- IsHD = request.IsHD,
- Is4K = request.Is4K,
- Tags = request.GetTags(),
- OfficialRatings = request.GetOfficialRatings(),
- Genres = request.GetGenres(),
- ArtistIds = GetGuids(request.ArtistIds),
- AlbumArtistIds = GetGuids(request.AlbumArtistIds),
- ContributingArtistIds = GetGuids(request.ContributingArtistIds),
- GenreIds = GetGuids(request.GenreIds),
- StudioIds = GetGuids(request.StudioIds),
- Person = request.Person,
- PersonIds = GetGuids(request.PersonIds),
- PersonTypes = request.GetPersonTypes(),
- Years = request.GetYears(),
- ImageTypes = request.GetImageTypes(),
- VideoTypes = request.GetVideoTypes(),
- AdjacentTo = request.AdjacentTo,
- ItemIds = GetGuids(request.Ids),
- MinCommunityRating = request.MinCommunityRating,
- MinCriticRating = request.MinCriticRating,
- ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId),
- ParentIndexNumber = request.ParentIndexNumber,
- EnableTotalRecordCount = request.EnableTotalRecordCount,
- ExcludeItemIds = GetGuids(request.ExcludeItemIds),
- DtoOptions = dtoOptions,
- SearchTerm = request.SearchTerm
- };
-
- if (!string.IsNullOrWhiteSpace(request.Ids) || !string.IsNullOrWhiteSpace(request.SearchTerm))
- {
- query.CollapseBoxSetItems = false;
- }
-
- foreach (var filter in request.GetFilters())
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- if (!string.IsNullOrEmpty(request.MinDateLastSaved))
- {
- query.MinDateLastSaved = DateTime.Parse(request.MinDateLastSaved, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- if (!string.IsNullOrEmpty(request.MinDateLastSavedForUser))
- {
- query.MinDateLastSavedForUser = DateTime.Parse(request.MinDateLastSavedForUser, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- if (!string.IsNullOrEmpty(request.MinPremiereDate))
- {
- query.MinPremiereDate = DateTime.Parse(request.MinPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- if (!string.IsNullOrEmpty(request.MaxPremiereDate))
- {
- query.MaxPremiereDate = DateTime.Parse(request.MaxPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- }
-
- // Filter by Series Status
- if (!string.IsNullOrEmpty(request.SeriesStatus))
- {
- query.SeriesStatuses = request.SeriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
- }
-
- // ExcludeLocationTypes
- if (!string.IsNullOrEmpty(request.ExcludeLocationTypes))
- {
- var excludeLocationTypes = request.ExcludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray();
- if (excludeLocationTypes.Contains(LocationType.Virtual))
- {
- query.IsVirtualItem = false;
- }
- }
-
- if (!string.IsNullOrEmpty(request.LocationTypes))
- {
- var requestedLocationTypes =
- request.LocationTypes.Split(',');
-
- if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
- {
- query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
- }
- }
-
- // Min official rating
- if (!string.IsNullOrWhiteSpace(request.MinOfficialRating))
- {
- query.MinParentalRating = _localization.GetRatingLevel(request.MinOfficialRating);
- }
-
- // Max official rating
- if (!string.IsNullOrWhiteSpace(request.MaxOfficialRating))
- {
- query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating);
- }
-
- // Artists
- if (!string.IsNullOrEmpty(request.Artists))
- {
- query.ArtistIds = request.Artists.Split('|').Select(i =>
- {
- try
- {
- return _libraryManager.GetArtist(i, new DtoOptions(false));
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null).Select(i => i.Id).ToArray();
- }
-
- // ExcludeArtistIds
- if (!string.IsNullOrWhiteSpace(request.ExcludeArtistIds))
- {
- query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds);
- }
-
- if (!string.IsNullOrWhiteSpace(request.AlbumIds))
- {
- query.AlbumIds = GetGuids(request.AlbumIds);
- }
-
- // Albums
- if (!string.IsNullOrEmpty(request.Albums))
- {
- query.AlbumIds = request.Albums.Split('|').SelectMany(i =>
- {
- return _libraryManager.GetItemIds(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
- Name = i,
- Limit = 1
- });
- }).ToArray();
- }
-
- // Studios
- if (!string.IsNullOrEmpty(request.Studios))
- {
- query.StudioIds = request.Studios.Split('|').Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != 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 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
- {
- query.OrderBy = new[]
- {
- new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending),
- new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
- };
- }
- }
-
- return query;
- }
- }
-
- /// <summary>
- /// Class DateCreatedComparer.
- /// </summary>
- public class DateCreatedComparer : IComparer<BaseItem>
- {
- /// <summary>
- /// Compares the specified x.
- /// </summary>
- /// <param name="x">The x.</param>
- /// <param name="y">The y.</param>
- /// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
- {
- return x.DateCreated.CompareTo(y.DateCreated);
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs
deleted file mode 100644
index e9caca14a..000000000
--- a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- [Route("/MusicGenres", "GET", Summary = "Gets all music genres from a given item, folder, or the entire library")]
- public class GetMusicGenres : GetItemsByName
- {
- }
-
- [Route("/MusicGenres/{Name}", "GET", Summary = "Gets a music genre, by name")]
- public class GetMusicGenre : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- [Authenticated]
- public class MusicGenresService : BaseItemsByNameService<MusicGenre>
- {
- public MusicGenresService(
- ILogger<MusicGenresService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IAuthorizationContext authorizationContext)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- userDataRepository,
- dtoService,
- authorizationContext)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetMusicGenre request)
- {
- var result = GetItem(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the item.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{BaseItemDto}.</returns>
- private BaseItemDto GetItem(GetMusicGenre request)
- {
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- var item = GetMusicGenre(request.Name, LibraryManager, dtoOptions);
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- var user = UserManager.GetUserById(request.UserId);
-
- return DtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- return DtoService.GetBaseItemDto(item, dtoOptions);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetMusicGenres request)
- {
- var result = GetResultSlim(request);
-
- return ToOptimizedResult(result);
- }
-
- protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
- {
- return LibraryManager.GetMusicGenres(query);
- }
-
- /// <summary>
- /// Gets all items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="items">The items.</param>
- /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
- protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs
deleted file mode 100644
index 7924339ed..000000000
--- a/MediaBrowser.Api/UserLibrary/PersonsService.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class GetPersons.
- /// </summary>
- [Route("/Persons", "GET", Summary = "Gets all persons from a given item, folder, or the entire library")]
- public class GetPersons : GetItemsByName
- {
- }
-
- /// <summary>
- /// Class GetPerson.
- /// </summary>
- [Route("/Persons/{Name}", "GET", Summary = "Gets a person, by name")]
- public class GetPerson : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- /// <summary>
- /// Class PersonsService.
- /// </summary>
- [Authenticated]
- public class PersonsService : BaseItemsByNameService<Person>
- {
- public PersonsService(
- ILogger<PersonsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IAuthorizationContext authorizationContext)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- userDataRepository,
- dtoService,
- authorizationContext)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetPerson request)
- {
- var result = GetItem(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the item.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{BaseItemDto}.</returns>
- private BaseItemDto GetItem(GetPerson request)
- {
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- var item = GetPerson(request.Name, LibraryManager, dtoOptions);
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- var user = UserManager.GetUserById(request.UserId);
-
- return DtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- return DtoService.GetBaseItemDto(item, dtoOptions);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetPersons request)
- {
- return GetResultSlim(request);
- }
-
- /// <summary>
- /// Gets all items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="items">The items.</param>
- /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
- protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
- {
- throw new NotImplementedException();
- }
-
- protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
- {
- var items = LibraryManager.GetPeopleItems(new InternalPeopleQuery
- {
- PersonTypes = query.PersonTypes,
- NameContains = query.NameContains ?? query.SearchTerm
- });
-
- if ((query.IsFavorite ?? false) && query.User != null)
- {
- items = items.Where(i => UserDataRepository.GetUserData(query.User, i).IsFavorite).ToList();
- }
-
- return new QueryResult<(BaseItem, ItemCounts)>
- {
- TotalRecordCount = items.Count,
- Items = items.Take(query.Limit ?? int.MaxValue).Select(i => (i as BaseItem, new ItemCounts())).ToArray()
- };
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs
deleted file mode 100644
index d809cc2e7..000000000
--- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs
+++ /dev/null
@@ -1,456 +0,0 @@
-using System;
-using System.Globalization;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class MarkPlayedItem.
- /// </summary>
- [Route("/Users/{UserId}/PlayedItems/{Id}", "POST", Summary = "Marks an item as played")]
- public class MarkPlayedItem : IReturn<UserItemDataDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string UserId { get; set; }
-
- [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string DatePlayed { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class MarkUnplayedItem.
- /// </summary>
- [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE", Summary = "Marks an item as unplayed")]
- public class MarkUnplayedItem : IReturn<UserItemDataDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/Sessions/Playing", "POST", Summary = "Reports playback has started within a session")]
- public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid
- {
- }
-
- [Route("/Sessions/Playing/Progress", "POST", Summary = "Reports playback progress within a session")]
- public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid
- {
- }
-
- [Route("/Sessions/Playing/Ping", "POST", Summary = "Pings a playback session")]
- public class PingPlaybackSession : IReturnVoid
- {
- [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string PlaySessionId { get; set; }
- }
-
- [Route("/Sessions/Playing/Stopped", "POST", Summary = "Reports playback has stopped within a session")]
- public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid
- {
- }
-
- /// <summary>
- /// Class OnPlaybackStart.
- /// </summary>
- [Route("/Users/{UserId}/PlayingItems/{Id}", "POST", Summary = "Reports that a user has begun playing an item")]
- public class OnPlaybackStart : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public bool CanSeek { get; set; }
-
- [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
- public int? AudioStreamIndex { get; set; }
-
- [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
- public int? SubtitleStreamIndex { get; set; }
-
- [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public PlayMethod PlayMethod { get; set; }
-
- [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string LiveStreamId { get; set; }
-
- [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string PlaySessionId { get; set; }
- }
-
- /// <summary>
- /// Class OnPlaybackProgress.
- /// </summary>
- [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST", Summary = "Reports a user's playback progress")]
- public class OnPlaybackProgress : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string MediaSourceId { get; set; }
-
- /// <summary>
- /// Gets or sets the position ticks.
- /// </summary>
- /// <value>The position ticks.</value>
- [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
- public long? PositionTicks { get; set; }
-
- [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public bool IsPaused { get; set; }
-
- [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public bool IsMuted { get; set; }
-
- [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
- public int? AudioStreamIndex { get; set; }
-
- [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
- public int? SubtitleStreamIndex { get; set; }
-
- [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
- public int? VolumeLevel { get; set; }
-
- [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public PlayMethod PlayMethod { get; set; }
-
- [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string LiveStreamId { get; set; }
-
- [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string PlaySessionId { get; set; }
-
- [ApiMember(Name = "RepeatMode", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public RepeatMode RepeatMode { get; set; }
- }
-
- /// <summary>
- /// Class OnPlaybackStopped.
- /// </summary>
- [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE", Summary = "Reports that a user has stopped playing an item")]
- public class OnPlaybackStopped : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "NextMediaType", Description = "The next media type that will play", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string NextMediaType { get; set; }
-
- /// <summary>
- /// Gets or sets the position ticks.
- /// </summary>
- /// <value>The position ticks.</value>
- [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
- public long? PositionTicks { get; set; }
-
- [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string LiveStreamId { get; set; }
-
- [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string PlaySessionId { get; set; }
- }
-
- [Authenticated]
- public class PlaystateService : BaseApiService
- {
- private readonly IUserManager _userManager;
- private readonly IUserDataManager _userDataRepository;
- private readonly ILibraryManager _libraryManager;
- private readonly ISessionManager _sessionManager;
- private readonly ISessionContext _sessionContext;
- private readonly IAuthorizationContext _authContext;
-
- public PlaystateService(
- ILogger<PlaystateService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- IUserDataManager userDataRepository,
- ILibraryManager libraryManager,
- ISessionManager sessionManager,
- ISessionContext sessionContext,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _userDataRepository = userDataRepository;
- _libraryManager = libraryManager;
- _sessionManager = sessionManager;
- _sessionContext = sessionContext;
- _authContext = authContext;
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Post(MarkPlayedItem request)
- {
- var result = MarkPlayed(request);
-
- return ToOptimizedResult(result);
- }
-
- private UserItemDataDto MarkPlayed(MarkPlayedItem request)
- {
- var user = _userManager.GetUserById(Guid.Parse(request.UserId));
-
- DateTime? datePlayed = null;
-
- if (!string.IsNullOrEmpty(request.DatePlayed))
- {
- datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
- }
-
- var session = GetSession(_sessionContext);
-
- var dto = UpdatePlayedStatus(user, request.Id, true, datePlayed);
-
- foreach (var additionalUserInfo in session.AdditionalUsers)
- {
- var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
-
- UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed);
- }
-
- return dto;
- }
-
- private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
- {
- if (method == PlayMethod.Transcode)
- {
- var job = string.IsNullOrWhiteSpace(playSessionId) ? null : ApiEntryPoint.Instance.GetTranscodingJob(playSessionId);
- if (job == null)
- {
- return PlayMethod.DirectPlay;
- }
- }
-
- return method;
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(OnPlaybackStart request)
- {
- Post(new ReportPlaybackStart
- {
- CanSeek = request.CanSeek,
- ItemId = new Guid(request.Id),
- MediaSourceId = request.MediaSourceId,
- AudioStreamIndex = request.AudioStreamIndex,
- SubtitleStreamIndex = request.SubtitleStreamIndex,
- PlayMethod = request.PlayMethod,
- PlaySessionId = request.PlaySessionId,
- LiveStreamId = request.LiveStreamId
- });
- }
-
- public void Post(ReportPlaybackStart request)
- {
- request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId);
-
- request.SessionId = GetSession(_sessionContext).Id;
-
- var task = _sessionManager.OnPlaybackStart(request);
-
- Task.WaitAll(task);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public void Post(OnPlaybackProgress request)
- {
- Post(new ReportPlaybackProgress
- {
- ItemId = new Guid(request.Id),
- PositionTicks = request.PositionTicks,
- IsMuted = request.IsMuted,
- IsPaused = request.IsPaused,
- MediaSourceId = request.MediaSourceId,
- AudioStreamIndex = request.AudioStreamIndex,
- SubtitleStreamIndex = request.SubtitleStreamIndex,
- VolumeLevel = request.VolumeLevel,
- PlayMethod = request.PlayMethod,
- PlaySessionId = request.PlaySessionId,
- LiveStreamId = request.LiveStreamId,
- RepeatMode = request.RepeatMode
- });
- }
-
- public void Post(ReportPlaybackProgress request)
- {
- request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId);
-
- request.SessionId = GetSession(_sessionContext).Id;
-
- var task = _sessionManager.OnPlaybackProgress(request);
-
- Task.WaitAll(task);
- }
-
- public void Post(PingPlaybackSession request)
- {
- ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId, null);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Delete(OnPlaybackStopped request)
- {
- return Post(new ReportPlaybackStopped
- {
- ItemId = new Guid(request.Id),
- PositionTicks = request.PositionTicks,
- MediaSourceId = request.MediaSourceId,
- PlaySessionId = request.PlaySessionId,
- LiveStreamId = request.LiveStreamId,
- NextMediaType = request.NextMediaType
- });
- }
-
- public async Task Post(ReportPlaybackStopped request)
- {
- Logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", request.PlaySessionId ?? string.Empty);
-
- if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
- {
- await ApiEntryPoint.Instance.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true);
- }
-
- request.SessionId = GetSession(_sessionContext).Id;
-
- await _sessionManager.OnPlaybackStopped(request);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Delete(MarkUnplayedItem request)
- {
- var task = MarkUnplayed(request);
-
- return ToOptimizedResult(task);
- }
-
- private UserItemDataDto MarkUnplayed(MarkUnplayedItem request)
- {
- var user = _userManager.GetUserById(Guid.Parse(request.UserId));
-
- var session = GetSession(_sessionContext);
-
- var dto = UpdatePlayedStatus(user, request.Id, false, null);
-
- foreach (var additionalUserInfo in session.AdditionalUsers)
- {
- var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
-
- UpdatePlayedStatus(additionalUser, request.Id, false, null);
- }
-
- return dto;
- }
-
- /// <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, string itemId, bool wasPlayed, DateTime? datePlayed)
- {
- var item = _libraryManager.GetItemById(itemId);
-
- if (wasPlayed)
- {
- item.MarkPlayed(user, datePlayed, true);
- }
- else
- {
- item.MarkUnplayed(user);
- }
-
- return _userDataRepository.GetUserDataDto(item, user);
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs
deleted file mode 100644
index 66350955f..000000000
--- a/MediaBrowser.Api/UserLibrary/StudiosService.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class GetStudios.
- /// </summary>
- [Route("/Studios", "GET", Summary = "Gets all studios from a given item, folder, or the entire library")]
- public class GetStudios : GetItemsByName
- {
- }
-
- /// <summary>
- /// Class GetStudio.
- /// </summary>
- [Route("/Studios/{Name}", "GET", Summary = "Gets a studio, by name")]
- public class GetStudio : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "Name", Description = "The studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- /// <summary>
- /// Class StudiosService.
- /// </summary>
- [Authenticated]
- public class StudiosService : BaseItemsByNameService<Studio>
- {
- public StudiosService(
- ILogger<StudiosService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IAuthorizationContext authorizationContext)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- userDataRepository,
- dtoService,
- authorizationContext)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetStudio request)
- {
- var result = GetItem(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the item.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{BaseItemDto}.</returns>
- private BaseItemDto GetItem(GetStudio request)
- {
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- var item = GetStudio(request.Name, LibraryManager, dtoOptions);
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- var user = UserManager.GetUserById(request.UserId);
-
- return DtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- return DtoService.GetBaseItemDto(item, dtoOptions);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetStudios request)
- {
- var result = GetResultSlim(request);
-
- return ToOptimizedResult(result);
- }
-
- protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
- {
- return LibraryManager.GetStudios(query);
- }
-
- /// <summary>
- /// Gets all items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="items">The items.</param>
- /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
- protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
deleted file mode 100644
index f9cbba410..000000000
--- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
+++ /dev/null
@@ -1,575 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class GetItem.
- /// </summary>
- [Route("/Users/{UserId}/Items/{Id}", "GET", Summary = "Gets an item from a user's library")]
- public class GetItem : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class GetItem.
- /// </summary>
- [Route("/Users/{UserId}/Items/Root", "GET", Summary = "Gets the root folder from a user's library")]
- public class GetRootFolder : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- /// <summary>
- /// Class GetIntros.
- /// </summary>
- [Route("/Users/{UserId}/Items/{Id}/Intros", "GET", Summary = "Gets intros to play before the main media item plays")]
- public class GetIntros : IReturn<QueryResult<BaseItemDto>>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the item id.
- /// </summary>
- /// <value>The item id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class MarkFavoriteItem.
- /// </summary>
- [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST", Summary = "Marks an item as a favorite")]
- public class MarkFavoriteItem : IReturn<UserItemDataDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class UnmarkFavoriteItem.
- /// </summary>
- [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE", Summary = "Unmarks an item as a favorite")]
- public class UnmarkFavoriteItem : IReturn<UserItemDataDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class ClearUserItemRating.
- /// </summary>
- [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE", Summary = "Deletes a user's saved personal rating for an item")]
- public class DeleteUserItemRating : IReturn<UserItemDataDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class UpdateUserItemRating.
- /// </summary>
- [Route("/Users/{UserId}/Items/{Id}/Rating", "POST", Summary = "Updates a user's rating for an item")]
- public class UpdateUserItemRating : IReturn<UserItemDataDto>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid Id { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
- /// </summary>
- /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
- [ApiMember(Name = "Likes", Description = "Whether the user likes the item or not. true/false", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")]
- public bool Likes { get; set; }
- }
-
- /// <summary>
- /// Class GetLocalTrailers.
- /// </summary>
- [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET", Summary = "Gets local trailers for an item")]
- public class GetLocalTrailers : IReturn<BaseItemDto[]>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- /// <summary>
- /// Class GetSpecialFeatures.
- /// </summary>
- [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET", Summary = "Gets special features for an item")]
- public class GetSpecialFeatures : IReturn<BaseItemDto[]>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")]
- public class GetLatestMedia : IReturn<BaseItemDto[]>, IHasDtoOptions
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int Limit { get; set; }
-
- [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid ParentId { get; set; }
-
- [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string Fields { get; set; }
-
- [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
- public string IncludeItemTypes { get; set; }
-
- [ApiMember(Name = "IsFolder", Description = "Filter by items that are folders, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsFolder { get; set; }
-
- [ApiMember(Name = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsPlayed { get; set; }
-
- [ApiMember(Name = "GroupItems", Description = "Whether or not to group items into a parent container.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool GroupItems { get; set; }
-
- [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableImages { get; set; }
-
- [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? ImageTypeLimit { get; set; }
-
- [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string EnableImageTypes { get; set; }
-
- [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? EnableUserData { get; set; }
-
- public GetLatestMedia()
- {
- Limit = 20;
- GroupItems = true;
- }
- }
-
- /// <summary>
- /// Class UserLibraryService.
- /// </summary>
- [Authenticated]
- public class UserLibraryService : BaseApiService
- {
- 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 IAuthorizationContext _authContext;
-
- public UserLibraryService(
- ILogger<UserLibraryService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IUserViewManager userViewManager,
- IFileSystem fileSystem,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _userDataRepository = userDataRepository;
- _dtoService = dtoService;
- _userViewManager = userViewManager;
- _fileSystem = fileSystem;
- _authContext = authContext;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetSpecialFeatures request)
- {
- var result = GetAsync(request);
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetLatestMedia request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- if (!request.IsPlayed.HasValue)
- {
- if (user.HidePlayedInLatest)
- {
- request.IsPlayed = false;
- }
- }
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var list = _userViewManager.GetLatestItems(new LatestItemsQuery
- {
- GroupItems = request.GroupItems,
- IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true),
- IsPlayed = request.IsPlayed,
- Limit = request.Limit,
- ParentId = request.ParentId,
- UserId = request.UserId,
- }, dtoOptions);
-
- var dtos = list.Select(i =>
- {
- var item = i.Item2[0];
- var childCount = 0;
-
- if (i.Item1 != null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
- {
- item = i.Item1;
- childCount = i.Item2.Count;
- }
-
- var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
- dto.ChildCount = childCount;
-
- return dto;
- });
-
- return ToOptimizedResult(dtos.ToArray());
- }
-
- private BaseItemDto[] GetAsync(GetSpecialFeatures request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var item = string.IsNullOrEmpty(request.Id) ?
- _libraryManager.GetUserRootFolder() :
- _libraryManager.GetItemById(request.Id);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var dtos = item
- .GetExtras(BaseItem.DisplayExtraTypes)
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item));
-
- return dtos.ToArray();
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetLocalTrailers request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer })
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
-
- if (item is IHasTrailers hasTrailers)
- {
- var trailers = hasTrailers.GetTrailers();
- var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
- var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
- dtosExtras.CopyTo(allTrailers, 0);
- dtosTrailers.CopyTo(allTrailers, dtosExtras.Length);
- return ToOptimizedResult(allTrailers);
- }
-
- return ToOptimizedResult(dtosExtras);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public async Task<object> Get(GetItem request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
- await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
- return ToOptimizedResult(result);
- }
-
- 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)
- {
- var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ForceSave = performFullRefresh
- };
-
- await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
- }
- }
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetRootFolder request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var item = _libraryManager.GetUserRootFolder();
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public async Task<object> Get(GetIntros request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
- var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = dtos,
- TotalRecordCount = dtos.Length
- };
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Post(MarkFavoriteItem request)
- {
- var dto = MarkFavorite(request.UserId, request.Id, true);
-
- return ToOptimizedResult(dto);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Delete(UnmarkFavoriteItem request)
- {
- var dto = MarkFavorite(request.UserId, request.Id, false);
-
- return ToOptimizedResult(dto);
- }
-
- /// <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)
- {
- var user = _userManager.GetUserById(userId);
-
- var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
-
- // 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>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Delete(DeleteUserItemRating request)
- {
- var dto = UpdateUserItemRating(request.UserId, request.Id, null);
-
- return ToOptimizedResult(dto);
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Post(UpdateUserItemRating request)
- {
- var dto = UpdateUserItemRating(request.UserId, request.Id, request.Likes);
-
- return ToOptimizedResult(dto);
- }
-
- /// <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 UpdateUserItemRating(Guid userId, Guid itemId, bool? likes)
- {
- var user = _userManager.GetUserById(userId);
-
- var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
-
- // 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);
- }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs
deleted file mode 100644
index 6f1620ddd..000000000
--- a/MediaBrowser.Api/UserLibrary/UserViewsService.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Library;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- [Route("/Users/{UserId}/Views", "GET")]
- public class GetUserViews : IReturn<QueryResult<BaseItemDto>>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
-
- [ApiMember(Name = "IncludeExternalContent", Description = "Whether or not to include external views such as channels or live tv", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "GET")]
- public bool? IncludeExternalContent { get; set; }
-
- public bool IncludeHidden { get; set; }
-
- public string PresetViews { get; set; }
- }
-
- [Route("/Users/{UserId}/GroupingOptions", "GET")]
- public class GetGroupingOptions : IReturn<SpecialViewOption[]>
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- public class UserViewsService : BaseApiService
- {
- private readonly IUserManager _userManager;
- private readonly IUserViewManager _userViewManager;
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
- private readonly ILibraryManager _libraryManager;
-
- public UserViewsService(
- ILogger<UserViewsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- IUserViewManager userViewManager,
- IDtoService dtoService,
- IAuthorizationContext authContext,
- ILibraryManager libraryManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _userViewManager = userViewManager;
- _dtoService = dtoService;
- _authContext = authContext;
- _libraryManager = libraryManager;
- }
-
- public object Get(GetUserViews request)
- {
- var query = new UserViewQuery
- {
- UserId = request.UserId
- };
-
- if (request.IncludeExternalContent.HasValue)
- {
- query.IncludeExternalContent = request.IncludeExternalContent.Value;
- }
-
- query.IncludeHidden = request.IncludeHidden;
-
- if (!string.IsNullOrWhiteSpace(request.PresetViews))
- {
- query.PresetViews = request.PresetViews.Split(',');
- }
-
- var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
- if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
- {
- query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
- }
-
- var folders = _userViewManager.GetUserViews(query);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
- var fields = dtoOptions.Fields.ToList();
-
- fields.Add(ItemFields.PrimaryImageAspectRatio);
- fields.Add(ItemFields.DisplayPreferencesId);
- fields.Remove(ItemFields.BasicSyncInfo);
- dtoOptions.Fields = fields.ToArray();
-
- var user = _userManager.GetUserById(request.UserId);
-
- var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
- .ToArray();
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = dtos,
- TotalRecordCount = dtos.Length
- };
-
- return ToOptimizedResult(result);
- }
-
- public object Get(GetGroupingOptions request)
- {
- var user = _userManager.GetUserById(request.UserId);
-
- var list = _libraryManager.GetUserRootFolder()
- .GetChildren(user, true)
- .OfType<Folder>()
- .Where(UserView.IsEligibleForGrouping)
- .Select(i => new SpecialViewOption
- {
- Name = i.Name,
- Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
- })
- .OrderBy(i => i.Name)
- .ToArray();
-
- return ToOptimizedResult(list);
- }
- }
-
- class SpecialViewOption
- {
- public string Name { get; set; }
-
- public string Id { get; set; }
- }
-}
diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs
deleted file mode 100644
index 0523f89fa..000000000
--- a/MediaBrowser.Api/UserLibrary/YearsService.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
- /// <summary>
- /// Class GetYears.
- /// </summary>
- [Route("/Years", "GET", Summary = "Gets all years from a given item, folder, or the entire library")]
- public class GetYears : GetItemsByName
- {
- }
-
- /// <summary>
- /// Class GetYear.
- /// </summary>
- [Route("/Years/{Year}", "GET", Summary = "Gets a year")]
- public class GetYear : IReturn<BaseItemDto>
- {
- /// <summary>
- /// Gets or sets the year.
- /// </summary>
- /// <value>The year.</value>
- [ApiMember(Name = "Year", Description = "The year", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
- public int Year { get; set; }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- /// <summary>
- /// Class YearsService.
- /// </summary>
- [Authenticated]
- public class YearsService : BaseItemsByNameService<Year>
- {
- public YearsService(
- ILogger<YearsService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IUserDataManager userDataRepository,
- IDtoService dtoService,
- IAuthorizationContext authorizationContext)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- userDataRepository,
- dtoService,
- authorizationContext)
- {
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetYear request)
- {
- var result = GetItem(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the item.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{BaseItemDto}.</returns>
- private BaseItemDto GetItem(GetYear request)
- {
- var item = LibraryManager.GetYear(request.Year);
-
- var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
- if (!request.UserId.Equals(Guid.Empty))
- {
- var user = UserManager.GetUserById(request.UserId);
-
- return DtoService.GetBaseItemDto(item, dtoOptions, user);
- }
-
- return DtoService.GetBaseItemDto(item, dtoOptions);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetYears request)
- {
- var result = GetResult(request);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets all items.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="items">The items.</param>
- /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
- protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
- {
- return items
- .Select(i => i.ProductionYear ?? 0)
- .Where(i => i > 0)
- .Distinct()
- .Select(year => LibraryManager.GetYear(year));
- }
- }
-}
diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs
deleted file mode 100644
index 6e9d788dc..000000000
--- a/MediaBrowser.Api/UserService.cs
+++ /dev/null
@@ -1,598 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <summary>
- /// Class GetUsers.
- /// </summary>
- [Route("/Users", "GET", Summary = "Gets a list of users")]
- [Authenticated]
- public class GetUsers : IReturn<UserDto[]>
- {
- [ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsHidden { get; set; }
-
- [ApiMember(Name = "IsDisabled", Description = "Optional filter by IsDisabled=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsDisabled { get; set; }
-
- [ApiMember(Name = "IsGuest", Description = "Optional filter by IsGuest=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool? IsGuest { get; set; }
- }
-
- [Route("/Users/Public", "GET", Summary = "Gets a list of publicly visible users for display on a login screen.")]
- public class GetPublicUsers : IReturn<UserDto[]>
- {
- }
-
- /// <summary>
- /// Class GetUser.
- /// </summary>
- [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")]
- [Authenticated(EscapeParentalControl = true)]
- public class GetUser : IReturn<UserDto>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class DeleteUser.
- /// </summary>
- [Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")]
- [Authenticated(Roles = "Admin")]
- public class DeleteUser : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class AuthenticateUser.
- /// </summary>
- [Route("/Users/{Id}/Authenticate", "POST", Summary = "Authenticates a user")]
- public class AuthenticateUser : IReturn<AuthenticationResult>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Pw { get; set; }
-
- /// <summary>
- /// Gets or sets the password.
- /// </summary>
- /// <value>The password.</value>
- [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Password { get; set; }
- }
-
- /// <summary>
- /// Class AuthenticateUser.
- /// </summary>
- [Route("/Users/AuthenticateByName", "POST", Summary = "Authenticates a user")]
- public class AuthenticateUserByName : IReturn<AuthenticationResult>
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Username", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Username { get; set; }
-
- /// <summary>
- /// Gets or sets the password.
- /// </summary>
- /// <value>The password.</value>
- [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Password { get; set; }
-
- [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Pw { get; set; }
- }
-
- /// <summary>
- /// Class UpdateUserPassword.
- /// </summary>
- [Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")]
- [Authenticated]
- public class UpdateUserPassword : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public Guid Id { get; set; }
-
- /// <summary>
- /// Gets or sets the password.
- /// </summary>
- /// <value>The password.</value>
- public string CurrentPassword { get; set; }
-
- public string CurrentPw { get; set; }
-
- public string NewPw { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [reset password].
- /// </summary>
- /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
- public bool ResetPassword { get; set; }
- }
-
- /// <summary>
- /// Class UpdateUserEasyPassword.
- /// </summary>
- [Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")]
- [Authenticated]
- public class UpdateUserEasyPassword : IReturnVoid
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public Guid Id { get; set; }
-
- /// <summary>
- /// Gets or sets the new password.
- /// </summary>
- /// <value>The new password.</value>
- public string NewPassword { get; set; }
-
- public string NewPw { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [reset password].
- /// </summary>
- /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
- public bool ResetPassword { get; set; }
- }
-
- /// <summary>
- /// Class UpdateUser.
- /// </summary>
- [Route("/Users/{Id}", "POST", Summary = "Updates a user")]
- [Authenticated]
- public class UpdateUser : UserDto, IReturnVoid
- {
- }
-
- /// <summary>
- /// Class UpdateUser.
- /// </summary>
- [Route("/Users/{Id}/Policy", "POST", Summary = "Updates a user policy")]
- [Authenticated(Roles = "admin")]
- public class UpdateUserPolicy : UserPolicy, IReturnVoid
- {
- [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class UpdateUser.
- /// </summary>
- [Route("/Users/{Id}/Configuration", "POST", Summary = "Updates a user configuration")]
- [Authenticated]
- public class UpdateUserConfiguration : UserConfiguration, IReturnVoid
- {
- [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public Guid Id { get; set; }
- }
-
- /// <summary>
- /// Class CreateUser.
- /// </summary>
- [Route("/Users/New", "POST", Summary = "Creates a user")]
- [Authenticated(Roles = "Admin")]
- public class CreateUserByName : IReturn<UserDto>
- {
- [ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Name { get; set; }
-
- [ApiMember(Name = "Password", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Password { get; set; }
- }
-
- [Route("/Users/ForgotPassword", "POST", Summary = "Initiates the forgot password process for a local user")]
- public class ForgotPassword : IReturn<ForgotPasswordResult>
- {
- [ApiMember(Name = "EnteredUsername", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string EnteredUsername { get; set; }
- }
-
- [Route("/Users/ForgotPassword/Pin", "POST", Summary = "Redeems a forgot password pin")]
- public class ForgotPasswordPin : IReturn<PinRedeemResult>
- {
- [ApiMember(Name = "Pin", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
- public string Pin { get; set; }
- }
-
- /// <summary>
- /// Class UsersService.
- /// </summary>
- public class UserService : BaseApiService
- {
- /// <summary>
- /// The user manager.
- /// </summary>
- private readonly IUserManager _userManager;
- private readonly ISessionManager _sessionMananger;
- private readonly INetworkManager _networkManager;
- private readonly IDeviceManager _deviceManager;
- private readonly IAuthorizationContext _authContext;
-
- public UserService(
- ILogger<UserService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- ISessionManager sessionMananger,
- INetworkManager networkManager,
- IDeviceManager deviceManager,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _sessionMananger = sessionMananger;
- _networkManager = networkManager;
- _deviceManager = deviceManager;
- _authContext = authContext;
- }
-
- public object Get(GetPublicUsers request)
- {
- // If the startup wizard hasn't been completed then just return all users
- if (!ServerConfigurationManager.Configuration.IsStartupWizardCompleted)
- {
- return Get(new GetUsers
- {
- IsDisabled = false
- });
- }
-
- return Get(new GetUsers
- {
- IsHidden = false,
- IsDisabled = false
- }, true, true);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetUsers request)
- {
- return Get(request, false, false);
- }
-
- private object Get(GetUsers request, bool filterByDevice, bool filterByNetwork)
- {
- var users = _userManager.Users;
-
- if (request.IsDisabled.HasValue)
- {
- users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value);
- }
-
- if (request.IsHidden.HasValue)
- {
- users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.IsHidden.Value);
- }
-
- if (filterByDevice)
- {
- var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
-
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
- }
- }
-
- if (filterByNetwork)
- {
- if (!_networkManager.IsInLocalNetwork(Request.RemoteIp))
- {
- users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
- }
- }
-
- var result = users
- .OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
- .ToArray();
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetUser request)
- {
- var user = _userManager.GetUserById(request.Id);
-
- if (user == null)
- {
- throw new ResourceNotFoundException("User not found");
- }
-
- var result = _userManager.GetUserDto(user, Request.RemoteIp);
-
- return ToOptimizedResult(result);
- }
-
- /// <summary>
- /// Deletes the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Delete(DeleteUser request)
- {
- return DeleteAsync(request);
- }
-
- public Task DeleteAsync(DeleteUser request)
- {
- _userManager.DeleteUser(request.Id);
- _sessionMananger.RevokeUserTokens(request.Id, null);
- return Task.CompletedTask;
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public object Post(AuthenticateUser request)
- {
- var user = _userManager.GetUserById(request.Id);
-
- if (user == null)
- {
- throw new ResourceNotFoundException("User not found");
- }
-
- if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw))
- {
- throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API.");
- }
-
- // Password should always be null
- return Post(new AuthenticateUserByName
- {
- Username = user.Username,
- Password = null,
- Pw = request.Pw
- });
- }
-
- public async Task<object> Post(AuthenticateUserByName request)
- {
- var auth = _authContext.GetAuthorizationInfo(Request);
-
- try
- {
- var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
- {
- App = auth.Client,
- AppVersion = auth.Version,
- DeviceId = auth.DeviceId,
- DeviceName = auth.Device,
- Password = request.Pw,
- PasswordSha1 = request.Password,
- RemoteEndPoint = Request.RemoteIp,
- Username = request.Username
- }).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
- catch (SecurityException e)
- {
- // rethrow adding IP address to message
- throw new SecurityException($"[{Request.RemoteIp}] {e.Message}", e);
- }
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public Task Post(UpdateUserPassword request)
- {
- return PostAsync(request);
- }
-
- public async Task PostAsync(UpdateUserPassword request)
- {
- AssertCanUpdateUser(_authContext, _userManager, request.Id, true);
-
- var user = _userManager.GetUserById(request.Id);
-
- if (user == null)
- {
- throw new ResourceNotFoundException("User not found");
- }
-
- if (request.ResetPassword)
- {
- await _userManager.ResetPassword(user).ConfigureAwait(false);
- }
- else
- {
- var success = await _userManager.AuthenticateUser(
- user.Username,
- request.CurrentPw,
- request.CurrentPassword,
- Request.RemoteIp,
- false).ConfigureAwait(false);
-
- if (success == null)
- {
- throw new ArgumentException("Invalid user or password entered.");
- }
-
- await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
-
- var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
-
- _sessionMananger.RevokeUserTokens(user.Id, currentToken);
- }
- }
-
- public void Post(UpdateUserEasyPassword request)
- {
- AssertCanUpdateUser(_authContext, _userManager, request.Id, true);
-
- var user = _userManager.GetUserById(request.Id);
-
- if (user == null)
- {
- throw new ResourceNotFoundException("User not found");
- }
-
- if (request.ResetPassword)
- {
- _userManager.ResetEasyPassword(user);
- }
- else
- {
- _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
- }
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- public async Task Post(UpdateUser request)
- {
- var id = Guid.Parse(GetPathValue(1));
-
- AssertCanUpdateUser(_authContext, _userManager, id, false);
-
- var dtoUser = request;
-
- var user = _userManager.GetUserById(id);
-
- if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal))
- {
- await _userManager.UpdateUserAsync(user);
- _userManager.UpdateConfiguration(user.Id, dtoUser.Configuration);
- }
- else
- {
- await _userManager.RenameUser(user, dtoUser.Name).ConfigureAwait(false);
-
- _userManager.UpdateConfiguration(dtoUser.Id, dtoUser.Configuration);
- }
- }
-
- /// <summary>
- /// Posts the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public async Task<object> Post(CreateUserByName request)
- {
- var newUser = _userManager.CreateUser(request.Name);
-
- // no need to authenticate password for new user
- if (request.Password != null)
- {
- await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
- }
-
- var result = _userManager.GetUserDto(newUser, Request.RemoteIp);
-
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(ForgotPassword request)
- {
- var isLocal = Request.IsLocal || _networkManager.IsInLocalNetwork(Request.RemoteIp);
-
- var result = await _userManager.StartForgotPasswordProcess(request.EnteredUsername, isLocal).ConfigureAwait(false);
-
- return result;
- }
-
- public async Task<object> Post(ForgotPasswordPin request)
- {
- var result = await _userManager.RedeemPasswordResetPin(request.Pin).ConfigureAwait(false);
-
- return result;
- }
-
- public void Post(UpdateUserConfiguration request)
- {
- AssertCanUpdateUser(_authContext, _userManager, request.Id, false);
-
- _userManager.UpdateConfiguration(request.Id, request);
- }
-
- public void Post(UpdateUserPolicy request)
- {
- var user = _userManager.GetUserById(request.Id);
-
- // If removing admin access
- if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
- {
- if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
- {
- throw new ArgumentException("There must be at least one user in the system with administrative access.");
- }
- }
-
- // If disabling
- if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
- {
- throw new ArgumentException("Administrators cannot be disabled.");
- }
-
- // If disabling
- if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
- {
- if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
- {
- throw new ArgumentException("There must be at least one enabled user in the system.");
- }
-
- var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
- _sessionMananger.RevokeUserTokens(user.Id, currentToken);
- }
-
- _userManager.UpdatePolicy(request.Id, request);
- }
- }
-}
diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs
deleted file mode 100644
index 957a279f8..000000000
--- a/MediaBrowser.Api/VideosService.cs
+++ /dev/null
@@ -1,193 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Videos/{Id}/AdditionalParts", "GET", Summary = "Gets additional parts for a video.")]
- [Authenticated]
- public class GetAdditionalParts : IReturn<QueryResult<BaseItemDto>>
- {
- [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- [Route("/Videos/{Id}/AlternateSources", "DELETE", Summary = "Removes alternate video sources.")]
- [Authenticated(Roles = "Admin")]
- public class DeleteAlternateSources : IReturnVoid
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- [Route("/Videos/MergeVersions", "POST", Summary = "Merges videos into a single record")]
- [Authenticated(Roles = "Admin")]
- public class MergeVersions : IReturnVoid
- {
- [ApiMember(Name = "Ids", Description = "Item id list. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
- public string Ids { get; set; }
- }
-
- public class VideosService : BaseApiService
- {
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
-
- public VideosService(
- ILogger<VideosService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- _authContext = authContext;
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetAdditionalParts request)
- {
- var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
- var item = string.IsNullOrEmpty(request.Id)
- ? (!request.UserId.Equals(Guid.Empty)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.RootFolder)
- : _libraryManager.GetItemById(request.Id);
-
- var dtoOptions = GetDtoOptions(_authContext, request);
-
- BaseItemDto[] items;
- if (item is Video video)
- {
- items = video.GetAdditionalParts()
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
- .ToArray();
- }
- else
- {
- items = Array.Empty<BaseItemDto>();
- }
-
- var result = new QueryResult<BaseItemDto>
- {
- Items = items,
- TotalRecordCount = items.Length
- };
-
- return ToOptimizedResult(result);
- }
-
- public void Delete(DeleteAlternateSources request)
- {
- var video = (Video)_libraryManager.GetItemById(request.Id);
-
- foreach (var link in video.GetLinkedAlternateVersions())
- {
- link.SetPrimaryVersionId(null);
- link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
-
- link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
- }
-
- video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- video.SetPrimaryVersionId(null);
- video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
- }
-
- public void Post(MergeVersions request)
- {
- var items = request.Ids.Split(',')
- .Select(i => _libraryManager.GetItemById(i))
- .OfType<Video>()
- .OrderBy(i => i.Id)
- .ToList();
-
- if (items.Count < 2)
- {
- throw new ArgumentException("Please supply at least two videos to merge.");
- }
-
- var videosWithVersions = items.Where(i => i.MediaSourceCount > 1)
- .ToList();
-
- var primaryVersion = videosWithVersions.FirstOrDefault();
- if (primaryVersion == null)
- {
- primaryVersion = items.OrderBy(i =>
- {
- if (i.Video3DFormat.HasValue || i.VideoType != Model.Entities.VideoType.VideoFile)
- {
- return 1;
- }
-
- return 0;
- })
- .ThenByDescending(i =>
- {
- return i.GetDefaultVideoStream()?.Width ?? 0;
- }).First();
- }
-
- var list = primaryVersion.LinkedAlternateVersions.ToList();
-
- foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
- {
- item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
-
- item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-
- list.Add(new LinkedChild
- {
- Path = item.Path,
- ItemId = item.Id
- });
-
- foreach (var linkedItem in item.LinkedAlternateVersions)
- {
- if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
- {
- list.Add(linkedItem);
- }
- }
-
- if (item.LinkedAlternateVersions.Length > 0)
- {
- item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
- }
- }
-
- primaryVersion.LinkedAlternateVersions = list.ToArray();
- primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
- }
- }
-}
diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
new file mode 100644
index 000000000..d746207c7
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Static class containing extension methods for <see cref="HttpContext"/>.
+ /// </summary>
+ public static class HttpContextExtensions
+ {
+ private const string ServiceStackRequest = "ServiceStackRequest";
+
+ /// <summary>
+ /// Set the ServiceStack request.
+ /// </summary>
+ /// <param name="httpContext">The HttpContext instance.</param>
+ /// <param name="request">The service stack request instance.</param>
+ public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request)
+ {
+ httpContext.Items[ServiceStackRequest] = request;
+ }
+
+ /// <summary>
+ /// Get the ServiceStack request.
+ /// </summary>
+ /// <param name="httpContext">The HttpContext instance.</param>
+ /// <returns>The service stack request instance.</returns>
+ public static IRequest GetServiceStackRequest(this HttpContext httpContext)
+ {
+ return (IRequest)httpContext.Items[ServiceStackRequest];
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
new file mode 100644
index 000000000..56c0ecbe9
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Double to String JSON converter.
+ /// Web client send quoted doubles.
+ /// </summary>
+ public class JsonDoubleConverter : JsonConverter<double>
+ {
+ /// <summary>
+ /// Read JSON string as double.
+ /// </summary>
+ /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
+ /// <param name="typeToConvert">Type.</param>
+ /// <param name="options">Options.</param>
+ /// <returns>Parsed value.</returns>
+ public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ // try to parse number directly from bytes
+ var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+ if (Utf8Parser.TryParse(span, out double number, out var bytesConsumed) && span.Length == bytesConsumed)
+ {
+ return number;
+ }
+
+ // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters
+ if (double.TryParse(reader.GetString(), out number))
+ {
+ return number;
+ }
+ }
+
+ // fallback to default handling
+ return reader.GetDouble();
+ }
+
+ /// <summary>
+ /// Write double to JSON string.
+ /// </summary>
+ /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
+ /// <param name="value">Value to write.</param>
+ /// <param name="options">Options.</param>
+ public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
+ {
+ writer.WriteNumberValue(value);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
index fe5dd6cd4..70c375b8c 100644
--- a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
@@ -14,40 +14,27 @@ namespace MediaBrowser.Common.Json.Converters
/// <inheritdoc />
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- static void ThrowFormatException() => throw new FormatException("Invalid format for an integer.");
- ReadOnlySpan<byte> span = stackalloc byte[0];
-
- if (reader.HasValueSequence)
- {
- long sequenceLength = reader.ValueSequence.Length;
- Span<byte> stackSpan = stackalloc byte[(int)sequenceLength];
- reader.ValueSequence.CopyTo(stackSpan);
- span = stackSpan;
- }
- else
+ if (reader.TokenType == JsonTokenType.String)
{
- span = reader.ValueSpan;
- }
+ ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+ if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed)
+ {
+ return number;
+ }
- if (!Utf8Parser.TryParse(span, out int number, out _))
- {
- ThrowFormatException();
+ if (int.TryParse(reader.GetString(), out number))
+ {
+ return number;
+ }
}
- return number;
+ return reader.GetInt32();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
- static void ThrowInvalidOperationException() => throw new InvalidOperationException();
- Span<byte> span = stackalloc byte[16];
- if (Utf8Formatter.TryFormat(value, span, out int bytesWritten))
- {
- writer.WriteStringValue(span.Slice(0, bytesWritten));
- }
-
- ThrowInvalidOperationException();
+ writer.WriteNumberValue(value);
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs
new file mode 100644
index 000000000..427f1fa7e
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Parse JSON string as long.
+ /// Javascript does not support 64-bit integers.
+ /// </summary>
+ public class JsonInt64Converter : JsonConverter<long>
+ {
+ /// <summary>
+ /// Read JSON string as int64.
+ /// </summary>
+ /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
+ /// <param name="type">Type.</param>
+ /// <param name="options">Options.</param>
+ /// <returns>Parsed value.</returns>
+ public override long Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ // try to parse number directly from bytes
+ var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+ if (Utf8Parser.TryParse(span, out long number, out var bytesConsumed) && span.Length == bytesConsumed)
+ {
+ return number;
+ }
+
+ // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters
+ if (long.TryParse(reader.GetString(), out number))
+ {
+ return number;
+ }
+ }
+
+ // fallback to default handling
+ return reader.GetInt64();
+ }
+
+ /// <summary>
+ /// Write long to JSON long.
+ /// </summary>
+ /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
+ /// <param name="value">Value to write.</param>
+ /// <param name="options">Options.</param>
+ public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
+ {
+ writer.WriteNumberValue(value);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
index 0a36e1cb2..8053461f0 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Json.Converters
/// <param name="typeToConvert">The type to convert.</param>
/// <param name="options">The json serializer options.</param>
/// <returns>Typed dictionary.</returns>
- /// <exception cref="NotSupportedException">Not supported.</exception>
+ /// <exception cref="NotSupportedException">Dictionary key type not supported.</exception>
public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]);
@@ -72,7 +72,7 @@ namespace MediaBrowser.Common.Json.Converters
{
if (k != null)
{
- convertedDictionary[k.ToString()] = v;
+ convertedDictionary[k.ToString()] = v;
}
}
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 78a458add..36ab6d900 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -12,20 +12,51 @@ namespace MediaBrowser.Common.Json
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions" /> options.
/// </summary>
+ /// <remarks>
+ /// When changing these options, update
+ /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+ /// -> AddJellyfinApi
+ /// -> AddJsonOptions.
+ /// </remarks>
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetOptions()
{
- var options = new JsonSerializerOptions()
+ var options = new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Disallow,
WriteIndented = false
};
options.Converters.Add(new JsonGuidConverter());
+ options.Converters.Add(new JsonInt32Converter());
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
+ options.Converters.Add(new JsonInt64Converter());
+ options.Converters.Add(new JsonDoubleConverter());
return options;
}
+
+ /// <summary>
+ /// Gets camelCase json options.
+ /// </summary>
+ /// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
+ public static JsonSerializerOptions GetCamelCaseOptions()
+ {
+ var options = GetOptions();
+ options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ return options;
+ }
+
+ /// <summary>
+ /// Gets PascalCase json options.
+ /// </summary>
+ /// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
+ public static JsonSerializerOptions GetPascalCaseOptions()
+ {
+ var options = GetOptions();
+ options.PropertyNamingPolicy = null;
+ return options;
+ }
}
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index c9ca153c7..7380f39fd 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -17,8 +17,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index b10233c71..15c902777 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -13,8 +13,6 @@ namespace MediaBrowser.Controller.Authentication
Task<ProviderAuthenticationResult> Authenticate(string username, string password);
bool HasPassword(User user);
Task ChangePassword(User user, string newPassword);
- void ChangeEasyPassword(User user, string newPassword, string newPasswordHash);
- string GetEasyPasswordHash(User user);
}
public interface IRequiresResolvedUser
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index 0dadc283e..988557f42 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -2,7 +2,6 @@ using System.Collections.Generic;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Dto
{
@@ -12,20 +11,6 @@ namespace MediaBrowser.Controller.Dto
public interface IDtoService
{
/// <summary>
- /// Gets the dto id.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>System.String.</returns>
- string GetDtoId(BaseItem item);
-
- /// <summary>
- /// Attaches the primary image aspect ratio.
- /// </summary>
- /// <param name="dto">The dto.</param>
- /// <param name="item">The item.</param>
- void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item);
-
- /// <summary>
/// Gets the primary image aspect ratio.
/// </summary>
/// <param name="item">The item.</param>
@@ -36,15 +21,6 @@ namespace MediaBrowser.Controller.Dto
/// Gets the base item dto.
/// </summary>
/// <param name="item">The item.</param>
- /// <param name="fields">The fields.</param>
- /// <param name="user">The user.</param>
- /// <param name="owner">The owner.</param>
- BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null);
-
- /// <summary>
- /// Gets the base item dto.
- /// </summary>
- /// <param name="item">The item.</param>
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 25933bc90..f34309c40 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -613,7 +613,7 @@ namespace MediaBrowser.Controller.Entities
{
if (!IsFileProtocol)
{
- return new string[] { };
+ return Array.Empty<string>();
}
return new[] { Path };
@@ -675,11 +675,11 @@ namespace MediaBrowser.Controller.Entities
return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
}
- var idString = Id.ToString("N", CultureInfo.InvariantCulture);
+ ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
basePath = System.IO.Path.Combine(basePath, "library");
- return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString);
+ return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString);
}
/// <summary>
@@ -702,26 +702,27 @@ namespace MediaBrowser.Controller.Entities
foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
{
- sortable = sortable.Replace(removeChar, string.Empty);
+ sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
}
foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
{
- sortable = sortable.Replace(replaceChar, " ");
+ sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
}
foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
{
// Remove from beginning if a space follows
- if (sortable.StartsWith(search + " "))
+ if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
{
sortable = sortable.Remove(0, search.Length + 1);
}
+
// Remove from middle if surrounded by spaces
- sortable = sortable.Replace(" " + search + " ", " ");
+ sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
// Remove from end if followed by a space
- if (sortable.EndsWith(" " + search))
+ if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
{
sortable = sortable.Remove(sortable.Length - (search.Length + 1));
}
@@ -751,6 +752,7 @@ namespace MediaBrowser.Controller.Entities
builder.Append(chunkBuilder);
}
+
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
return builder.ToString().RemoveDiacritics();
}
diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
index 688439e6c..6a350212b 100644
--- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
+++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
@@ -1,4 +1,7 @@
+#pragma warning disable CS1591
+
using System;
+using System.Collections.Generic;
namespace MediaBrowser.Controller.Entities
{
@@ -8,6 +11,6 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the special feature ids.
/// </summary>
/// <value>The special feature ids.</value>
- Guid[] SpecialFeatureIds { get; set; }
+ IReadOnlyList<Guid> SpecialFeatureIds { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 70c48b6f1..c131c5430 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -198,7 +198,7 @@ namespace MediaBrowser.Controller.Entities.Movies
public Guid[] GetLibraryFolderIds()
{
- var expandedFolders = new List<Guid>() { };
+ var expandedFolders = new List<Guid>();
return FlattenItems(this, expandedFolders)
.SelectMany(i => LibraryManager.GetCollectionFolders(i))
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 53badac4d..8b67aaccc 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -1,5 +1,8 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
@@ -17,8 +20,6 @@ namespace MediaBrowser.Controller.Entities.Movies
/// </summary>
public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
{
- public Guid[] SpecialFeatureIds { get; set; }
-
public Movie()
{
SpecialFeatureIds = Array.Empty<Guid>();
@@ -28,6 +29,9 @@ namespace MediaBrowser.Controller.Entities.Movies
}
/// <inheritdoc />
+ public IReadOnlyList<Guid> SpecialFeatureIds { get; set; }
+
+ /// <inheritdoc />
public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
/// <inheritdoc />
@@ -46,6 +50,9 @@ namespace MediaBrowser.Controller.Entities.Movies
set => TmdbCollectionName = value;
}
+ [JsonIgnore]
+ public override bool StopRefreshIfLocalMetadataFound => false;
+
public override double GetDefaultPrimaryImageAspectRatio()
{
// hack for tv plugins
@@ -105,6 +112,7 @@ namespace MediaBrowser.Controller.Entities.Movies
return itemsChanged;
}
+ /// <inheritdoc />
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Movie;
@@ -133,6 +141,7 @@ namespace MediaBrowser.Controller.Entities.Movies
return info;
}
+ /// <inheritdoc />
public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
@@ -169,6 +178,7 @@ namespace MediaBrowser.Controller.Entities.Movies
return hasChanges;
}
+ /// <inheritdoc />
public override List<ExternalUrl> GetRelatedUrls()
{
var list = base.GetRelatedUrls();
@@ -179,14 +189,11 @@ namespace MediaBrowser.Controller.Entities.Movies
list.Add(new ExternalUrl
{
Name = "Trakt",
- Url = string.Format("https://trakt.tv/movies/{0}", imdbId)
+ Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
});
}
return list;
}
-
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 45daa8a53..23d960092 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -496,7 +496,7 @@ namespace MediaBrowser.Controller.Entities.TV
list.Add(new ExternalUrl
{
Name = "Trakt",
- Url = string.Format("https://trakt.tv/shows/{0}", imdbId)
+ Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId)
});
}
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 6b544afc6..83e9ce1e7 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
@@ -86,7 +87,7 @@ namespace MediaBrowser.Controller.Entities
list.Add(new ExternalUrl
{
Name = "Trakt",
- Url = string.Format("https://trakt.tv/movies/{0}", imdbId)
+ Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
});
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index cb35d6e32..e3f4025bb 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -1,8 +1,11 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
@@ -671,9 +674,7 @@ namespace MediaBrowser.Controller.Entities
var isPlaceHolder = false;
- var hasPlaceHolder = item as ISupportsPlaceHolders;
-
- if (hasPlaceHolder != null)
+ if (item is ISupportsPlaceHolders hasPlaceHolder)
{
isPlaceHolder = hasPlaceHolder.IsPlaceHolder;
}
@@ -688,13 +689,11 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasSpecialFeature.Value;
- var movie = item as IHasSpecialFeatures;
-
- if (movie != null)
+ if (item is IHasSpecialFeatures movie)
{
var ok = filterValue
- ? movie.SpecialFeatureIds.Length > 0
- : movie.SpecialFeatureIds.Length == 0;
+ ? movie.SpecialFeatureIds.Count > 0
+ : movie.SpecialFeatureIds.Count == 0;
if (!ok)
{
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index c2932cc4c..4c2209b67 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -34,6 +34,16 @@ namespace MediaBrowser.Controller.Extensions
public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
/// <summary>
+ /// The key for a setting that indicates whether kestrel should bind to a unix socket.
+ /// </summary>
+ public const string BindToUnixSocketKey = "kestrel:socket";
+
+ /// <summary>
+ /// The key for the unix socket path.
+ /// </summary>
+ public const string UnixSocketPathKey = "kestrel:socketPath";
+
+ /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
@@ -65,5 +75,21 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>True if playlists should allow duplicates, otherwise false.</returns>
public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration)
=> configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey);
+
+ /// <summary>
+ /// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
+ /// </summary>
+ /// <param name="configuration">The configuration to read the setting from.</param>
+ /// <returns><c>true</c> if kestrel should bind to a unix socket, otherwise <c>false</c>.</returns>
+ public static bool UseUnixSocket(this IConfiguration configuration)
+ => configuration.GetValue<bool>(BindToUnixSocketKey);
+
+ /// <summary>
+ /// Gets the path for the unix socket from the <see cref="IConfiguration" />.
+ /// </summary>
+ /// <param name="configuration">The configuration to read the setting from.</param>
+ /// <returns>The unix socket path.</returns>
+ public static string GetUnixSocketPath(this IConfiguration configuration)
+ => configuration[UnixSocketPathKey];
}
}
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
new file mode 100644
index 000000000..b6bfed3e5
--- /dev/null
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Entities;
+
+namespace MediaBrowser.Controller
+{
+ /// <summary>
+ /// Manages the storage and retrieval of display preferences.
+ /// </summary>
+ public interface IDisplayPreferencesManager
+ {
+ /// <summary>
+ /// Gets the display preferences for the user and client.
+ /// </summary>
+ /// <param name="userId">The user's id.</param>
+ /// <param name="client">The client string.</param>
+ /// <returns>The associated display preferences.</returns>
+ DisplayPreferences GetDisplayPreferences(Guid userId, string client);
+
+ /// <summary>
+ /// Gets the default item display preferences for the user and client.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client string.</param>
+ /// <returns>The item display preferences.</returns>
+ ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client);
+
+ /// <summary>
+ /// Gets all of the item display preferences for the user and client.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="client">The client string.</param>
+ /// <returns>A list of item display preferences.</returns>
+ IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
+
+ /// <summary>
+ /// Saves changes to the provided display preferences.
+ /// </summary>
+ /// <param name="preferences">The display preferences to save.</param>
+ void SaveChanges(DisplayPreferences preferences);
+
+ /// <summary>
+ /// Saves changes to the provided item display preferences.
+ /// </summary>
+ /// <param name="preferences">The item display preferences to save.</param>
+ void SaveChanges(ItemDisplayPreferences preferences);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 9d6646857..9abcf2b62 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -199,7 +200,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Updates the item.
/// </summary>
- void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+ void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index e73fe7120..6685861a9 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Events;
@@ -55,7 +54,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Initializes the user manager and ensures that a user exists.
/// </summary>
- void Initialize();
+ Task InitializeAsync();
/// <summary>
/// Gets a user by Id.
@@ -106,7 +105,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>The created user.</returns>
/// <exception cref="ArgumentNullException">name</exception>
/// <exception cref="ArgumentException"></exception>
- User CreateUser(string name);
+ Task<User> CreateUserAsync(string name);
/// <summary>
/// Deletes the specified user.
@@ -166,8 +165,6 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
- void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
-
NameIdPair[] GetAuthenticationProviders();
NameIdPair[] GetPasswordResetProviders();
diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs
index 399378a09..5efdc6a48 100644
--- a/MediaBrowser.Controller/Library/Profiler.cs
+++ b/MediaBrowser.Controller/Library/Profiler.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Library
@@ -13,6 +14,7 @@ namespace MediaBrowser.Controller.Library
/// The name.
/// </summary>
readonly string _name;
+
/// <summary>
/// The stopwatch.
/// </summary>
@@ -44,6 +46,7 @@ namespace MediaBrowser.Controller.Library
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
/// <summary>
@@ -58,13 +61,19 @@ namespace MediaBrowser.Controller.Library
string message;
if (_stopwatch.ElapsedMilliseconds > 300000)
{
- message = string.Format("{0} took {1} minutes.",
- _name, ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F"));
+ message = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} took {1} minutes.",
+ _name,
+ ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F", CultureInfo.InvariantCulture));
}
else
{
- message = string.Format("{0} took {1} seconds.",
- _name, ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000"));
+ message = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} took {1} seconds.",
+ _name,
+ ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000", CultureInfo.InvariantCulture));
}
_logger.LogInformation(message);
diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs
index fc9b3f1c6..a3aa6019e 100644
--- a/MediaBrowser.Controller/Library/TVUtils.cs
+++ b/MediaBrowser.Controller/Library/TVUtils.cs
@@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Library
};
}
- return new DayOfWeek[] { };
+ return Array.Empty<DayOfWeek>();
}
return null;
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index 10af98121..aa7c12dd1 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.LiveTv
if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
{
- return string.Format("{0:00000.0}", number) + "-" + (Name ?? string.Empty);
+ return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty);
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 472b061e6..e1de01ff0 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -261,7 +261,7 @@ namespace MediaBrowser.Controller.LiveTv
list.Add(new ExternalUrl
{
Name = "Trakt",
- Url = string.Format("https://trakt.tv/movies/{0}", imdbId)
+ Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
});
}
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 73e966344..67f17f7a5 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -13,8 +13,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index d3fb6a46d..7b09f489e 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Jellyfin.Data.Enums;
@@ -371,7 +372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public int GetVideoProfileScore(string profile)
{
// strip spaces because they may be stripped out on the query string
- profile = profile.Replace(" ", "");
+ profile = profile.Replace(" ", string.Empty, StringComparison.Ordinal);
return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
}
@@ -449,41 +450,60 @@ namespace MediaBrowser.Controller.MediaEncoding
var arg = new StringBuilder();
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
- bool isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
- bool isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
- bool isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
- bool isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
- if (state.IsVideoRequest
- && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ if (!IsCopyCodec(outputVideoCodec))
{
- if (isVaapiDecoder)
+ if (state.IsVideoRequest
+ && _mediaEncoder.SupportsHwaccel("vaapi")
+ && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{
- arg.Append("-hwaccel_output_format vaapi ")
- .Append("-vaapi_device ")
- .Append(encodingOptions.VaapiDevice)
- .Append(" ");
- }
- else if (!isVaapiDecoder && isVaapiEncoder)
- {
- arg.Append("-vaapi_device ")
- .Append(encodingOptions.VaapiDevice)
- .Append(" ");
+ if (isVaapiDecoder)
+ {
+ arg.Append("-hwaccel_output_format vaapi ")
+ .Append("-vaapi_device ")
+ .Append(encodingOptions.VaapiDevice)
+ .Append(' ');
+ }
+ else if (!isVaapiDecoder && isVaapiEncoder)
+ {
+ arg.Append("-vaapi_device ")
+ .Append(encodingOptions.VaapiDevice)
+ .Append(' ');
+ }
}
- }
- if (state.IsVideoRequest
- && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
- {
- var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
- if (!hasTextSubs)
+ if (state.IsVideoRequest
+ && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
- if (isQsvEncoder)
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
+ if (isQsvEncoder)
{
if (isQsvDecoder)
{
- arg.Append("-hwaccel qsv ");
+ if (isLinux)
+ {
+ if (hasGraphicalSubs)
+ {
+ arg.Append("-init_hw_device qsv=hw -filter_hw_device hw ");
+ }
+ else
+ {
+ arg.Append("-hwaccel qsv ");
+ }
+ }
+
+ if (isWindows)
+ {
+ arg.Append("-hwaccel qsv ");
+ }
}
// While using SW decoder
else
@@ -492,6 +512,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
}
+
+ if (state.IsVideoRequest
+ && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ arg.Append("-hwaccel videotoolbox ");
+ }
}
arg.Append("-i ")
@@ -649,7 +675,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// }
// }
- // fallbackFontParam = string.Format(":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath)));
+ // fallbackFontParam = string.Format(CultureInfo.InvariantCulture, ":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath)));
if (state.SubtitleStream.IsExternal)
{
@@ -806,6 +832,34 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
}
}
+ else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (encodingOptions.EncoderPreset)
+ {
+ case "veryslow":
+ case "slow":
+ case "slower":
+ param += "-quality quality";
+ break;
+
+ case "medium":
+ param += "-quality balanced";
+ break;
+
+ case "fast":
+ case "faster":
+ case "veryfast":
+ case "superfast":
+ case "ultrafast":
+ param += "-quality speed";
+ break;
+
+ default:
+ param += "-quality speed";
+ break;
+ }
+ }
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{
// Values 0-3, 0 being highest quality but slower
@@ -826,7 +880,7 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/
- param += string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+ param += string.Format(CultureInfo.InvariantCulture, "-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
profileScore.ToString(_usCulture),
crf,
qmin,
@@ -850,7 +904,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var framerate = GetFramerateParam(state);
if (framerate.HasValue)
{
- param += string.Format(" -r {0}", framerate.Value.ToString(_usCulture));
+ param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(_usCulture));
}
var targetVideoCodec = state.ActualOutputVideoCodec;
@@ -1276,6 +1330,17 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
+ {
+ if (audioBitRate.HasValue)
+ {
+ // Don't encode any higher than this
+ return Math.Min(384000, audioBitRate.Value);
+ }
+
+ return null;
+ }
+
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
{
var channels = state.OutputAudioChannels;
@@ -1351,7 +1416,7 @@ namespace MediaBrowser.Controller.MediaEncoding
transcoderChannelLimit = 6;
}
- var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec);
+ var isTranscodingAudio = !IsCopyCodec(codec);
int? resultChannels = state.GetRequestedAudioChannels(codec);
if (isTranscodingAudio)
@@ -1419,7 +1484,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (time > 0)
{
- return string.Format("-ss {0}", _mediaEncoder.GetTimeParameter(time));
+ return string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(time));
}
return string.Empty;
@@ -1541,7 +1606,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
outputVideoCodec ??= string.Empty;
- var outputSizeParam = string.Empty;
+ var outputSizeParam = ReadOnlySpan<char>.Empty;
var request = state.BaseRequest;
// Add resolution params, if specified
@@ -1552,31 +1617,53 @@ namespace MediaBrowser.Controller.MediaEncoding
{
outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
- var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
+ // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
+ var index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
}
else
{
- index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+ // vpp_qsv
+ index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
}
else
{
- index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+ // hwdownload,format=p010le (hardware decode + software encode for vaapi)
+ index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
}
else
{
- index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+ // format=nv12|vaapi,hwupload,scale_vaapi
+ index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
+ }
+ else
+ {
+ // yadif,scale=expr
+ index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = outputSizeParam.Slice(index);
+ }
+ else
+ {
+ // scale=expr
+ index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = outputSizeParam.Slice(index);
+ }
+ }
}
}
}
@@ -1585,43 +1672,30 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoSizeParam = string.Empty;
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
// Setup subtitle scaling
if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
{
- videoSizeParam = string.Format(
- CultureInfo.InvariantCulture,
- "scale={0}:{1}",
- state.VideoStream.Width.Value,
- state.VideoStream.Height.Value);
+ // Adjust the size of graphical subtitles to fit the video stream.
+ var videoStream = state.VideoStream;
+ var inputWidth = videoStream?.Width;
+ var inputHeight = videoStream?.Height;
+ var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
- // For QSV, feed it into hardware encoder now
- if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ if (width.HasValue && height.HasValue)
{
- videoSizeParam += ",hwupload=extra_hw_frames=64";
- }
-
- // For VAAPI and CUVID decoder
- // these encoders cannot automatically adjust the size of graphical subtitles to fit the output video,
- // thus needs to be manually adjusted.
- if (videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
- || (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
- && (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
- || outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)))
- {
- var videoStream = state.VideoStream;
- var inputWidth = videoStream?.Width;
- var inputHeight = videoStream?.Height;
- var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
-
- if (width.HasValue && height.HasValue)
- {
- videoSizeParam = string.Format(
+ videoSizeParam = string.Format(
CultureInfo.InvariantCulture,
- "scale={0}:{1}",
+ "scale={0}x{1}",
width.Value,
height.Value);
- }
+ }
+
+ // For QSV, feed it into hardware encoder now
+ if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ videoSizeParam += ",hwupload=extra_hw_frames=64";
}
}
@@ -1634,7 +1708,10 @@ namespace MediaBrowser.Controller.MediaEncoding
: state.SubtitleStream.Index;
// Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
- var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"";
+ // Always put the scaler before the overlay for better performance
+ var retStr = !outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
@@ -1644,12 +1721,11 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- outputSizeParam = outputSizeParam.TrimStart(',');
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
+ else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
{
/*
@@ -1657,7 +1733,6 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- outputSizeParam = outputSizeParam.TrimStart(',');
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
}
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
@@ -1666,14 +1741,13 @@ namespace MediaBrowser.Controller.MediaEncoding
QSV in FFMpeg can now setup hardware overlay for transcodes.
For software decoding and hardware encoding option, frames must be hwuploaded into hardware
with fixed frame size.
+ Currently only supports linux.
*/
- if (videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1)
+ if (isLinux)
{
- retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
- }
- else
- {
- retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
+ retStr = !outputSizeParam.IsEmpty ?
+ " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" :
+ " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
}
}
@@ -1683,7 +1757,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mapPrefix,
subtitleStreamIndex,
state.VideoStream.Index,
- outputSizeParam,
+ outputSizeParam.ToString(),
videoSizeParam);
}
@@ -1745,10 +1819,8 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxWidth,
requestedMaxHeight);
- var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs))
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
&& width.HasValue
&& height.HasValue)
{
@@ -1757,7 +1829,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// output dimensions. Output dimensions are guaranteed to be even.
var outputWidth = width.Value;
var outputHeight = height.Value;
- var vaapi_or_qsv = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ? "qsv" : "vaapi";
+ var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+ var isDeintEnabled = state.DeInterlace("h264", true)
+ || state.DeInterlace("avc", true)
+ || state.DeInterlace("h265", true)
+ || state.DeInterlace("hevc", true);
if (!videoWidth.HasValue
|| outputWidth != videoWidth.Value
@@ -1765,17 +1841,24 @@ namespace MediaBrowser.Controller.MediaEncoding
|| outputHeight != videoHeight.Value)
{
// Force nv12 pixel format to enable 10-bit to 8-bit colour conversion.
+ // use vpp_qsv filter to avoid green bar when the fixed output size is requested.
filters.Add(
string.Format(
CultureInfo.InvariantCulture,
- "scale_{0}=w={1}:h={2}:format=nv12",
- vaapi_or_qsv,
+ "{0}=w={1}:h={2}:format=nv12{3}",
+ qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
outputWidth,
- outputHeight));
+ outputHeight,
+ (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
else
{
- filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv));
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}=format=nv12{1}",
+ qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
+ (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
}
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
@@ -1987,7 +2070,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
var request = state.BaseRequest;
-
var videoStream = state.VideoStream;
var filters = new List<string>();
@@ -1996,32 +2078,34 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream?.Height;
var threeDFormat = state.MediaSource.Video3DFormat;
+ var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
// When the input may or may not be hardware VAAPI decodable
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add("format=nv12|vaapi");
filters.Add("hwupload");
}
- // When the input may or may not be hardware QSV decodable
- else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
+ else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
{
- if (!hasTextSubs)
- {
- filters.Add("format=nv12|qsv");
- filters.Add("hwupload=extra_hw_frames=64");
- }
+ filters.Add("hwupload=extra_hw_frames=64");
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
- && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
{
var codec = videoStream.Codec.ToLowerInvariant();
- var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
- || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
+ var isColorDepth10 = IsColorDepth10(state);
// Assert 10-bit hardware VAAPI decodable
if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2046,49 +2130,49 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add hardware deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true))
+ if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
{
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
}
- else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- {
- if (!hasTextSubs)
- {
- filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv"));
- }
- }
}
// Add software deinterlace filter before scaling filter
- if (((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true))
- && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- || (hasTextSubs && state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)))
+ if (state.DeInterlace("h264", true)
+ || state.DeInterlace("avc", true)
+ || state.DeInterlace("h265", true)
+ || state.DeInterlace("hevc", true))
{
+ string deintParam;
var inputFramerate = videoStream?.RealFrameRate;
// If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
{
- filters.Add("yadif=1:-1:0");
+ deintParam = "yadif=1:-1:0";
}
else
{
- filters.Add("yadif=0:-1:0");
+ deintParam = "yadif=0:-1:0";
+ }
+
+ if (!string.IsNullOrEmpty(deintParam))
+ {
+ if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
+ {
+ filters.Add(deintParam);
+ }
}
}
// Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
- // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642)
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
+ if (isVaapiH264Encoder)
{
- if (state.SubtitleStream != null
- && state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ if (hasTextSubs)
{
// Test passed on Intel and AMD gfx
filters.Add("hwmap=mode=read+write");
@@ -2098,9 +2182,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var output = string.Empty;
- if (state.SubtitleStream != null
- && state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ if (hasTextSubs)
{
var subParam = GetTextSubtitleParam(state);
@@ -2108,7 +2190,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
// Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add("hwmap");
}
@@ -2125,7 +2207,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return output;
}
-
/// <summary>
/// Gets the number of threads.
/// </summary>
@@ -2262,7 +2343,7 @@ namespace MediaBrowser.Controller.MediaEncoding
flags.Add("+ignidx");
}
- if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec))
{
flags.Add("+genpts");
}
@@ -2288,7 +2369,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
inputModifier += " " + videoDecoder;
- if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
+ if (!IsCopyCodec(state.OutputVideoCodec)
+ && (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
{
var videoStream = state.VideoStream;
var inputWidth = videoStream?.Width;
@@ -2301,11 +2383,19 @@ namespace MediaBrowser.Controller.MediaEncoding
&& width.HasValue
&& height.HasValue)
{
- inputModifier += string.Format(
- CultureInfo.InvariantCulture,
- " -resize {0}x{1}",
- width.Value,
- height.Value);
+ if (width.HasValue && height.HasValue)
+ {
+ inputModifier += string.Format(
+ CultureInfo.InvariantCulture,
+ " -resize {0}x{1}",
+ width.Value,
+ height.Value);
+ }
+
+ if (state.DeInterlace("h264", true))
+ {
+ inputModifier += " -deint 1";
+ }
}
}
}
@@ -2521,21 +2611,21 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets the name of the output video codec.
+ /// Gets the ffmpeg option string for the hardware accelerated video decoder.
/// </summary>
+ /// <param name="state">The encoding job info.</param>
+ /// <param name="encodingOptions">The encoding options.</param>
+ /// <returns>The option string or null if none available.</returns>
protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
{
- var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
var videoStream = state.VideoStream;
- var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
- || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
-
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ if (videoStream == null)
{
return null;
}
+ var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
@@ -2544,10 +2634,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
- if (videoStream != null
- && !string.IsNullOrEmpty(videoStream.Codec)
- && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+ if (IsCopyCodec(state.OutputVideoCodec))
+ {
+ return null;
+ }
+
+ if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
{
+ var isColorDepth10 = IsColorDepth10(state);
+
// Only hevc and vp9 formats have 10-bit hardware decoder support now.
if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
@@ -2623,13 +2718,6 @@ namespace MediaBrowser.Controller.MediaEncoding
case "h264":
if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
{
- // cuvid decoder does not support 10-bit input.
- if ((videoStream.BitDepth ?? 8) > 8)
- {
- encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
- return null;
- }
-
return "-c:v h264_cuvid";
}
@@ -2896,21 +2984,24 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec)
{
- var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- if (!isWindows)
+ if (isLinux)
{
return "-hwaccel vaapi";
}
- else if (isWindows8orLater)
+
+ if (isWindows && isWindows8orLater)
{
return "-hwaccel d3d11va";
}
- else
+
+ if (isWindows && !isWindows8orLater)
{
return "-hwaccel dxva2";
}
@@ -3000,7 +3091,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -mpegts_m2ts_mode 1";
}
- if (EncodingHelper.IsCopyCodec(videoCodec))
+ if (IsCopyCodec(videoCodec))
{
if (state.VideoStream != null
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
@@ -3102,7 +3193,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var args = "-codec:a:0 " + codec;
- if (EncodingHelper.IsCopyCodec(codec))
+ if (IsCopyCodec(codec))
{
return args;
}
@@ -3179,5 +3270,42 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
}
+
+ public static bool IsColorDepth10(EncodingJobInfo state)
+ {
+ var result = false;
+ var videoStream = state.VideoStream;
+
+ if (videoStream != null)
+ {
+ if (!string.IsNullOrEmpty(videoStream.PixelFormat))
+ {
+ result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase);
+ if (result)
+ {
+ return true;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(videoStream.Profile))
+ {
+ result = videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
+ || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)
+ || videoStream.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase);
+ if (result)
+ {
+ return true;
+ }
+ }
+
+ result = (videoStream.BitDepth ?? 8) == 10;
+ if (result)
+ {
+ return true;
+ }
+ }
+
+ return result;
+ }
}
}
diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
index ad786f97b..87a7f7e10 100644
--- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
+++ b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
@@ -52,6 +52,8 @@ namespace MediaBrowser.Controller.Net
return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
+ public bool IgnoreLegacyAuth { get; set; }
+
public bool AllowLocalOnly { get; set; }
}
@@ -66,5 +68,7 @@ namespace MediaBrowser.Controller.Net
bool AllowLocalOnly { get; }
string[] GetRoles();
+
+ bool IgnoreLegacyAuth { get; }
}
}
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index 56737dc65..2055a656a 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -6,11 +6,25 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// IAuthService.
+ /// </summary>
public interface IAuthService
{
- void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
+ /// <summary>
+ /// Authenticate and authorize request.
+ /// </summary>
+ /// <param name="request">Request.</param>
+ /// <param name="authAttribtutes">Authorization attributes.</param>
+ void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
- User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues);
+ /// <summary>
+ /// Authenticate and authorize request.
+ /// </summary>
+ /// <param name="request">Request.</param>
+ /// <param name="authAttribtutes">Authorization attributes.</param>
+ /// <returns>Authenticated user.</returns>
+ User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
/// <summary>
/// Authenticate request.
diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
deleted file mode 100644
index c2dcb66d7..000000000
--- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Persistence
-{
- /// <summary>
- /// Interface IDisplayPreferencesRepository.
- /// </summary>
- public interface IDisplayPreferencesRepository : IRepository
- {
- /// <summary>
- /// Saves display preferences for an item.
- /// </summary>
- /// <param name="displayPreferences">The display preferences.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="client">The client.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveDisplayPreferences(
- DisplayPreferences displayPreferences,
- string userId,
- string client,
- CancellationToken cancellationToken);
-
- /// <summary>
- /// Saves all display preferences for a user.
- /// </summary>
- /// <param name="displayPreferences">The display preferences.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveAllDisplayPreferences(
- IEnumerable<DisplayPreferences> displayPreferences,
- Guid userId,
- CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets the display preferences.
- /// </summary>
- /// <param name="displayPreferencesId">The display preferences id.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="client">The client.</param>
- /// <returns>Task{DisplayPreferences}.</returns>
- DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client);
-
- /// <summary>
- /// Gets all display preferences for the given user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <returns>Task{DisplayPreferences}.</returns>
- IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId);
- }
-}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index b1a638883..0fd63770f 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -6,6 +6,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 955db0278..c77349d01 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -71,7 +72,7 @@ namespace MediaBrowser.Controller.Providers
/// <returns>Task.</returns>
Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken);
- Task SaveImage(User user, Stream source, string mimeType, string path);
+ Task SaveImage(Stream source, string mimeType, string path);
/// <summary>
/// Adds the metadata providers.
@@ -157,7 +158,7 @@ namespace MediaBrowser.Controller.Providers
/// <param name="url">The URL.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
- Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
+ Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
Dictionary<Guid, Guid> GetRefreshQueue();
diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
index 68a968f90..ee8f5b860 100644
--- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
+++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
@@ -34,6 +35,6 @@ namespace MediaBrowser.Controller.Providers
/// <param name="url">The URL.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
- Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken);
+ Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
index fdb0c8eb5..17ad9e4a3 100644
--- a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
+++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
@@ -1,3 +1,4 @@
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
@@ -12,6 +13,6 @@ namespace MediaBrowser.Controller.Providers
/// <param name="url">The URL.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
- Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken);
+ Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
index d0fac1efa..e742df517 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
@@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <summary>
/// Gets the default ping value used for sessions.
/// </summary>
- public long DefaulPing { get; } = 500;
+ public long DefaultPing { get; } = 500;
/// <summary>
/// Gets or sets the group identifier.
@@ -70,16 +70,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public void AddSession(SessionInfo session)
{
- if (ContainsSession(session.Id.ToString()))
+ if (ContainsSession(session.Id))
{
return;
}
var member = new GroupMember();
member.Session = session;
- member.Ping = DefaulPing;
+ member.Ping = DefaultPing;
member.IsBuffering = false;
- Participants[session.Id.ToString()] = member;
+ Participants[session.Id] = member;
}
/// <summary>
@@ -88,12 +88,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public void RemoveSession(SessionInfo session)
{
- if (!ContainsSession(session.Id.ToString()))
+ if (!ContainsSession(session.Id))
{
return;
}
- Participants.Remove(session.Id.ToString(), out _);
+ Participants.Remove(session.Id, out _);
}
/// <summary>
@@ -103,12 +103,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="ping">The ping.</param>
public void UpdatePing(SessionInfo session, long ping)
{
- if (!ContainsSession(session.Id.ToString()))
+ if (!ContainsSession(session.Id))
{
return;
}
- Participants[session.Id.ToString()].Ping = ping;
+ Participants[session.Id].Ping = ping;
}
/// <summary>
@@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <value name="session">The highest ping in the group.</value>
public long GetHighestPing()
{
- long max = Int64.MinValue;
+ long max = long.MinValue;
foreach (var session in Participants.Values)
{
max = Math.Max(max, session.Ping);
@@ -133,12 +133,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="isBuffering">The state.</param>
public void SetBuffering(SessionInfo session, bool isBuffering)
{
- if (!ContainsSession(session.Id.ToString()))
+ if (!ContainsSession(session.Id))
{
return;
}
- Participants[session.Id.ToString()].IsBuffering = isBuffering;
+ Participants[session.Id].IsBuffering = isBuffering;
}
/// <summary>
diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
index a3975c334..cde6f8e8c 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.SyncPlay
public class GroupMember
{
/// <summary>
- /// Gets or sets whether this member is buffering.
+ /// Gets or sets a value indicating whether this member is buffering.
/// </summary>
/// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
public bool IsBuffering { get; set; }
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
index de1fcd259..45c543806 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// </summary>
/// <param name="session">The session.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void InitGroup(SessionInfo session, CancellationToken cancellationToken);
+ void CreateGroup(SessionInfo session, CancellationToken cancellationToken);
/// <summary>
/// Adds the session to the group.
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index f02999370..a8ebe6bc5 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index e040286ab..4a54b677d 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Linq;
using BDInfo.IO;
@@ -5,7 +7,7 @@ using MediaBrowser.Model.IO;
namespace MediaBrowser.MediaEncoding.BdInfo
{
- class BdInfoDirectoryInfo : IDirectoryInfo
+ public class BdInfoDirectoryInfo : IDirectoryInfo
{
private readonly IFileSystem _fileSystem = null;
@@ -43,25 +45,32 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public IDirectoryInfo[] GetDirectories()
{
- return Array.ConvertAll(_fileSystem.GetDirectories(_impl.FullName).ToArray(),
+ return Array.ConvertAll(
+ _fileSystem.GetDirectories(_impl.FullName).ToArray(),
x => new BdInfoDirectoryInfo(_fileSystem, x));
}
public IFileInfo[] GetFiles()
{
- return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName).ToArray(),
+ return Array.ConvertAll(
+ _fileSystem.GetFiles(_impl.FullName).ToArray(),
x => new BdInfoFileInfo(x));
}
public IFileInfo[] GetFiles(string searchPattern)
{
- return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
+ return Array.ConvertAll(
+ _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
x => new BdInfoFileInfo(x));
}
public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption)
{
- return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false,
+ return Array.ConvertAll(
+ _fileSystem.GetFiles(
+ _impl.FullName,
+ new[] { searchPattern },
+ false,
searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(),
x => new BdInfoFileInfo(x));
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
index e6359f4fb..9108d9649 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
@@ -15,6 +15,10 @@ namespace MediaBrowser.MediaEncoding.BdInfo
{
private readonly IFileSystem _fileSystem;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
+ /// </summary>
+ /// <param name="fileSystem">The filesystem.</param>
public BdInfoExaminer(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
@@ -41,7 +45,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
var outputStream = new BlurayDiscInfo
{
- MediaStreams = new MediaStream[] { }
+ MediaStreams = Array.Empty<MediaStream>()
};
if (playlist == null)
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index a6ff4f767..0a8af8e9c 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -1,11 +1,18 @@
+#pragma warning disable CS1591
+
using System.IO;
using MediaBrowser.Model.IO;
namespace MediaBrowser.MediaEncoding.BdInfo
{
- class BdInfoFileInfo : BDInfo.IO.IFileInfo
+ public class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
- FileSystemMetadata _impl = null;
+ private FileSystemMetadata _impl = null;
+
+ public BdInfoFileInfo(FileSystemMetadata impl)
+ {
+ _impl = impl;
+ }
public string Name => _impl.Name;
@@ -17,14 +24,10 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public bool IsDir => _impl.IsDirectory;
- public BdInfoFileInfo(FileSystemMetadata impl)
- {
- _impl = impl;
- }
-
public System.IO.Stream OpenRead()
{
- return new FileStream(FullName,
+ return new FileStream(
+ FullName,
FileMode.Open,
FileAccess.Read,
FileShare.Read);
diff --git a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
index 75534b5bd..f81a337db 100644
--- a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
+++ b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
@@ -1,9 +1,7 @@
-using System;
+#pragma warning disable CS1591
+
using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
namespace MediaBrowser.MediaEncoding.Configuration
{
@@ -17,32 +15,4 @@ namespace MediaBrowser.MediaEncoding.Configuration
};
}
}
-
- public class EncodingConfigurationStore : ConfigurationStore, IValidatingConfiguration
- {
- public EncodingConfigurationStore()
- {
- ConfigurationType = typeof(EncodingOptions);
- Key = "encoding";
- }
-
- public void Validate(object oldConfig, object newConfig)
- {
- var newPath = ((EncodingOptions)newConfig).TranscodingTempPath;
-
- if (!string.IsNullOrWhiteSpace(newPath)
- && !string.Equals(((EncodingOptions)oldConfig).TranscodingTempPath, newPath, StringComparison.Ordinal))
- {
- // Validate
- if (!Directory.Exists(newPath))
- {
- throw new DirectoryNotFoundException(
- string.Format(
- CultureInfo.InvariantCulture,
- "{0} does not exist.",
- newPath));
- }
- }
- }
- }
}
diff --git a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs
new file mode 100644
index 000000000..2f158157e
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs
@@ -0,0 +1,38 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.MediaEncoding.Configuration
+{
+ public class EncodingConfigurationStore : ConfigurationStore, IValidatingConfiguration
+ {
+ public EncodingConfigurationStore()
+ {
+ ConfigurationType = typeof(EncodingOptions);
+ Key = "encoding";
+ }
+
+ public void Validate(object oldConfig, object newConfig)
+ {
+ var newPath = ((EncodingOptions)newConfig).TranscodingTempPath;
+
+ if (!string.IsNullOrWhiteSpace(newPath)
+ && !string.Equals(((EncodingOptions)oldConfig).TranscodingTempPath, newPath, StringComparison.Ordinal))
+ {
+ // Validate
+ if (!Directory.Exists(newPath))
+ {
+ throw new DirectoryNotFoundException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} does not exist.",
+ newPath));
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 5a7fabf84..0e9a5e502 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -102,6 +104,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
private static readonly IReadOnlyDictionary<string, Version> _ffmpegVersionMap = new Dictionary<string, Version>
{
+ { "libavutil=56.51,libavcodec=58.91,libavformat=58.45,libavdevice=58.10,libavfilter=7.85,libswscale=5.7,libswresample=3.7,libpostproc=55.7,", new Version(4, 3) },
{ "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) },
{ "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) },
{ "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) },
@@ -121,6 +124,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
_encoderPath = encoderPath;
}
+ private enum Codec
+ {
+ Encoder,
+ Decoder
+ }
+
public static Version MinVersion { get; } = new Version(4, 0);
public static Version MaxVersion { get; } = null;
@@ -207,12 +216,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// to parse. If this is not available, then we try to match known library versions to FFmpeg versions.
/// If that fails then we test the libraries to determine if they're newer than our minimum versions.
/// </summary>
- /// <param name="output"></param>
- /// <returns></returns>
+ /// <param name="output">The output from "ffmpeg -version".</param>
+ /// <returns>The FFmpeg version.</returns>
internal Version GetFFmpegVersion(string output)
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
- var match = Regex.Match(output, @"^ffmpeg version n?((?:\d+\.?)+)");
+ var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
if (match.Success)
{
@@ -268,11 +277,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <summary>
/// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
+ /// and condenses them on to one line. Output format is "name1=major.minor,name2=major.minor,etc.".
/// </summary>
- /// <param name="output"></param>
- /// <param name="versionString"></param>
- /// <param name="versionMap"></param>
- /// <returns></returns>
+ /// <param name="output">The 'ffmpeg -version' output.</param>
+ /// <returns>The library names and major.minor version numbers.</returns>
private static bool TryGetFFmpegLibraryVersions(string output, out string versionString, out IReadOnlyDictionary<string, Version> versionMap)
{
var sb = new StringBuilder(144);
@@ -281,7 +289,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
foreach (Match match in Regex.Matches(
output,
- @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))",
+ @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
RegexOptions.Multiline))
{
sb.Append(match.Groups["name"])
@@ -304,12 +312,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return sb.Length > 0;
}
- private enum Codec
- {
- Encoder,
- Decoder
- }
-
private IEnumerable<string> GetHwaccelTypes()
{
string output = null;
@@ -327,7 +329,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return Enumerable.Empty<string>();
}
- var found = output.Split(new char[] {'\r','\n'}, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
+ var found = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
_logger.LogInformation("Available hwaccel types: {Types}", found);
return found;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index d4aede572..082ae2888 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -1,4 +1,7 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using MediaBrowser.Model.MediaInfo;
@@ -12,7 +15,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
var url = inputFiles[0];
- return string.Format("\"{0}\"", url);
+ return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", url);
}
return GetConcatInputArgument(inputFiles);
@@ -31,7 +34,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
var files = string.Join("|", inputFiles.Select(NormalizePath));
- return string.Format("concat:\"{0}\"", files);
+ return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
}
// Determine the input path for video files
@@ -47,13 +50,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
if (path.IndexOf("://") != -1)
{
- return string.Format("\"{0}\"", path);
+ return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path);
}
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
path = NormalizePath(path);
- return string.Format("file:\"{0}\"", path);
+ return string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", path);
}
/// <summary>
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index a1a7f55c3..bdf2babfa 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1,5 +1,8 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -19,9 +22,8 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-using System.Diagnostics;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder
{
@@ -35,6 +37,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
internal const int DefaultImageExtractionTimeout = 5000;
+ /// <summary>
+ /// The us culture.
+ /// </summary>
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
private readonly ILogger<MediaEncoder> _logger;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
@@ -47,6 +54,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly object _runningProcessesLock = new object();
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
+ private List<string> _encoders = new List<string>();
+ private List<string> _decoders = new List<string>();
+ private List<string> _hwaccels = new List<string>();
+
private string _ffmpegPath = string.Empty;
private string _ffprobePath;
@@ -77,7 +88,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <summary>
/// Run at startup or if the user removes a Custom path from transcode page.
/// Sets global variables FFmpegPath.
- /// Precedence is: Config > CLI > $PATH
+ /// Precedence is: Config > CLI > $PATH.
/// </summary>
public void SetFFmpegPath()
{
@@ -122,8 +133,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
/// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
/// </summary>
- /// <param name="path"></param>
- /// <param name="pathType"></param>
+ /// <param name="path">The path.</param>
+ /// <param name="pathType">The path type.</param>
public void UpdateEncoderPath(string path, string pathType)
{
string newPath;
@@ -168,8 +179,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// If checks pass, global variable FFmpegPath and EncoderLocation are updated.
/// </summary>
/// <param name="path">FQPN to test.</param>
- /// <param name="location">Location (External, Custom, System) of tool</param>
- /// <returns></returns>
+ /// <param name="location">Location (External, Custom, System) of tool.</param>
+ /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
private bool ValidatePath(string path, FFmpegLocation location)
{
bool rc = false;
@@ -218,8 +229,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <summary>
/// Search the system $PATH environment variable looking for given filename.
/// </summary>
- /// <param name="fileName"></param>
- /// <returns></returns>
+ /// <param name="fileName">The filename.</param>
+ /// <returns>The full path to the file.</returns>
private string ExistsOnSystemPath(string fileName)
{
var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true);
@@ -243,25 +254,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
return null;
}
- private List<string> _encoders = new List<string>();
public void SetAvailableEncoders(IEnumerable<string> list)
{
_encoders = list.ToList();
- // _logger.Info("Supported encoders: {0}", string.Join(",", list.ToArray()));
}
- private List<string> _decoders = new List<string>();
public void SetAvailableDecoders(IEnumerable<string> list)
{
_decoders = list.ToList();
- // _logger.Info("Supported decoders: {0}", string.Join(",", list.ToArray()));
}
- private List<string> _hwaccels = new List<string>();
public void SetAvailableHwaccels(IEnumerable<string> list)
{
_hwaccels = list.ToList();
- //_logger.Info("Supported hwaccels: {0}", string.Join(",", list.ToArray()));
}
public bool SupportsEncoder(string encoder)
@@ -329,8 +334,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File;
- return GetMediaInfoInternal(GetInputArgument(inputFiles, request.MediaSource.Protocol), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters,
- probeSize, request.MediaType == DlnaProfileType.Audio, request.MediaSource.VideoType, forceEnableLogging, cancellationToken);
+ return GetMediaInfoInternal(
+ GetInputArgument(inputFiles, request.MediaSource.Protocol),
+ request.MediaSource.Path,
+ request.MediaSource.Protocol,
+ extractChapters,
+ probeSize,
+ request.MediaType == DlnaProfileType.Audio,
+ request.MediaSource.VideoType,
+ forceEnableLogging,
+ cancellationToken);
}
/// <summary>
@@ -339,7 +352,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <param name="inputFiles">The input files.</param>
/// <param name="protocol">The protocol.</param>
/// <returns>System.String.</returns>
- /// <exception cref="ArgumentException">Unrecognized InputType</exception>
+ /// <exception cref="ArgumentException">Unrecognized InputType.</exception>
public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaProtocol protocol)
=> EncodingUtils.GetInputArgument(inputFiles, protocol);
@@ -347,7 +360,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// Gets the media info internal.
/// </summary>
/// <returns>Task{MediaInfoResult}.</returns>
- private async Task<MediaInfo> GetMediaInfoInternal(string inputPath,
+ private async Task<MediaInfo> GetMediaInfoInternal(
+ string inputPath,
string primaryPath,
MediaProtocol protocol,
bool extractChapters,
@@ -375,7 +389,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
FileName = _ffprobePath,
Arguments = args,
-
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
},
@@ -436,11 +449,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- /// <summary>
- /// The us culture.
- /// </summary>
- protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
{
return ExtractImage(new[] { path }, null, null, imageStreamIndex, MediaProtocol.File, true, null, null, cancellationToken);
@@ -456,8 +464,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
return ExtractImage(inputFiles, container, imageStream, imageStreamIndex, protocol, false, null, null, cancellationToken);
}
- private async Task<string> ExtractImage(string[] inputFiles, string container, MediaStream videoStream, int? imageStreamIndex, MediaProtocol protocol, bool isAudio,
- Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
+ private async Task<string> ExtractImage(
+ string[] inputFiles,
+ string container,
+ MediaStream videoStream,
+ int? imageStreamIndex,
+ MediaProtocol protocol,
+ bool isAudio,
+ Video3DFormat? threedFormat,
+ TimeSpan? offset,
+ CancellationToken cancellationToken)
{
var inputArgument = GetInputArgument(inputFiles, protocol);
@@ -533,8 +549,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
var thumbnail = enableThumbnail ? ",thumbnail=24" : string.Empty;
- var args = useIFrame ? string.Format("-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) :
- string.Format("-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg);
+ var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) :
+ string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg);
var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1);
var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);
@@ -551,7 +567,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (offset.HasValue)
{
- args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args;
+ args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
}
if (videoStream != null)
@@ -622,7 +638,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1 || !file.Exists || file.Length == 0)
{
- var msg = string.Format("ffmpeg image extraction failed for {0}", inputPath);
+ var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath);
_logger.LogError(msg);
@@ -642,7 +658,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public string GetTimeParameter(TimeSpan time)
{
- return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
+ return time.ToString(@"hh\:mm\:ss\.fff", _usCulture);
}
public async Task ExtractVideoImagesOnInterval(
@@ -659,19 +675,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
var inputArgument = GetInputArgument(inputFiles, protocol);
- var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture);
+ var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture);
if (maxWidth.HasValue)
{
- var maxWidthParam = maxWidth.Value.ToString(UsCulture);
+ var maxWidthParam = maxWidth.Value.ToString(_usCulture);
- vf += string.Format(",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam);
+ vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam);
}
Directory.CreateDirectory(targetDirectory);
var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
- var args = string.Format("-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf);
+ var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf);
var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1);
var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);
@@ -771,7 +787,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1)
{
- var msg = string.Format("ffmpeg image extraction failed for {0}", inputArgument);
+ var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputArgument);
_logger.LogError(msg);
@@ -856,6 +872,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (dispose)
{
StopProcesses();
+ _thumbnailResourcePool.Dispose();
}
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index aeb4dbe73..017f917e2 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -9,6 +9,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
@@ -27,4 +28,16 @@
<PackageReference Include="UTF.Unknown" Version="2.3.0" />
</ItemGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <!-- Code Analyzers-->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <!-- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
</Project>
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 3aa296f7f..b2d4db894 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -3,6 +3,9 @@ using System.Collections.Generic;
namespace MediaBrowser.MediaEncoding.Probing
{
+ /// <summary>
+ /// Class containing helper methods for working with FFprobe output.
+ /// </summary>
public static class FFProbeHelpers
{
/// <summary>
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
index 6a45ccf49..de062d06b 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index a2ea0766a..8996d3b09 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -269,6 +269,10 @@ namespace MediaBrowser.MediaEncoding.Probing
[JsonPropertyName("loro_surmixlev")]
public string LoroSurmixlev { get; set; }
+ /// <summary>
+ /// Gets or sets the field_order.
+ /// </summary>
+ /// <value>The field_order.</value>
[JsonPropertyName("field_order")]
public string FieldOrder { get; set; }
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 3f21c2bd4..19e3bd8e6 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -16,10 +18,19 @@ namespace MediaBrowser.MediaEncoding.Probing
{
public class ProbeResultNormalizer
{
+ // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
+ private const int MaxSubtitleDescriptionExtractionLength = 100;
+
+ private const string ArtistReplaceValue = " | ";
+
+ private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
+
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
+ private List<string> _splitWhiteList = null;
+
public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization)
{
_logger = logger;
@@ -368,7 +379,6 @@ namespace MediaBrowser.MediaEncoding.Probing
private List<NameValuePair> ReadValueArray(XmlReader reader)
{
-
var pairs = new List<NameValuePair>();
reader.MoveToContent();
@@ -949,50 +959,46 @@ namespace MediaBrowser.MediaEncoding.Probing
private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
{
+ var peoples = new List<BaseItemPerson>();
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
if (!string.IsNullOrWhiteSpace(composer))
{
- var peoples = new List<BaseItemPerson>();
foreach (var person in Split(composer, false))
{
peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
}
-
- audio.People = peoples.ToArray();
}
- // var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor");
- // if (!string.IsNullOrWhiteSpace(conductor))
- //{
- // foreach (var person in Split(conductor, false))
- // {
- // audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
- // }
- //}
+ var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor");
+ if (!string.IsNullOrWhiteSpace(conductor))
+ {
+ foreach (var person in Split(conductor, false))
+ {
+ peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
+ }
+ }
- // var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist");
- // if (!string.IsNullOrWhiteSpace(lyricist))
- //{
- // foreach (var person in Split(lyricist, false))
- // {
- // audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
- // }
- //}
+ var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist");
+ if (!string.IsNullOrWhiteSpace(lyricist))
+ {
+ foreach (var person in Split(lyricist, false))
+ {
+ peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
+ }
+ }
// Check for writer some music is tagged that way as alternative to composer/lyricist
var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer");
if (!string.IsNullOrWhiteSpace(writer))
{
- var peoples = new List<BaseItemPerson>();
foreach (var person in Split(writer, false))
{
peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
}
-
- audio.People = peoples.ToArray();
}
+ audio.People = peoples.ToArray();
audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
@@ -1008,7 +1014,7 @@ namespace MediaBrowser.MediaEncoding.Probing
var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
if (string.IsNullOrWhiteSpace(artist))
{
- audio.Artists = new string[] { };
+ audio.Artists = Array.Empty<string>();
}
else
{
@@ -1031,7 +1037,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (string.IsNullOrWhiteSpace(albumArtist))
{
- audio.AlbumArtists = new string[] { };
+ audio.AlbumArtists = Array.Empty<string>();
}
else
{
@@ -1117,8 +1123,6 @@ namespace MediaBrowser.MediaEncoding.Probing
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
}
- private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
-
/// <summary>
/// Splits the specified val.
/// </summary>
@@ -1138,8 +1142,6 @@ namespace MediaBrowser.MediaEncoding.Probing
.Select(i => i.Trim());
}
- private const string ArtistReplaceValue = " | ";
-
private IEnumerable<string> SplitArtists(string val, char[] delimiters, bool splitFeaturing)
{
if (splitFeaturing)
@@ -1169,9 +1171,6 @@ namespace MediaBrowser.MediaEncoding.Probing
return artistsFound;
}
-
- private List<string> _splitWhiteList = null;
-
private IEnumerable<string> GetSplitWhitelist()
{
if (_splitWhiteList == null)
@@ -1248,7 +1247,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
/// <summary>
- /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
+ /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'.
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
@@ -1294,8 +1293,6 @@ namespace MediaBrowser.MediaEncoding.Probing
return info;
}
- private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
-
private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data)
{
if (data.Format == null || data.Format.Tags == null)
@@ -1367,7 +1364,9 @@ namespace MediaBrowser.MediaEncoding.Probing
// OR -> COMMENT. SUBTITLE: DESCRIPTION
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
- if (string.IsNullOrWhiteSpace(subTitle) && !string.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+ if (string.IsNullOrWhiteSpace(subTitle)
+ && !string.IsNullOrWhiteSpace(description)
+ && description.AsSpan().Slice(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] parts = description.Split(':');
if (parts.Length > 0)
@@ -1375,11 +1374,11 @@ namespace MediaBrowser.MediaEncoding.Probing
string subtitle = parts[0];
try
{
- if (subtitle.Contains("/")) // It contains a episode number and season number
+ if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number
{
string[] numbers = subtitle.Split(' ');
- video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
- int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
+ video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0]);
+ int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1]);
description = string.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
}
@@ -1390,8 +1389,11 @@ namespace MediaBrowser.MediaEncoding.Probing
}
catch // Default parsing
{
- if (subtitle.Contains(".")) // skip the comment, keep the subtitle
+ if (subtitle.Contains('.', StringComparison.Ordinal))
+ {
+ // skip the comment, keep the subtitle
description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+ }
else
{
description = subtitle.Trim(); // Clean up whitespaces and save it
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
index 0e2d70017..308b62886 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -13,6 +15,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ /// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
@@ -22,7 +25,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string line;
while (reader.ReadLine() != "[Events]")
- { }
+ {
+ }
var headers = ParseFieldHeaders(reader.ReadLine());
@@ -35,7 +39,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
continue;
}
- if (line.StartsWith("["))
+ if (line[0] == '[')
{
break;
}
@@ -62,7 +66,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return trackInfo;
}
- long GetTicks(string time)
+ private long GetTicks(ReadOnlySpan<char> time)
{
return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out var span)
? span.Ticks : 0;
@@ -72,17 +76,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
var fields = line.Substring(8).Split(',').Select(x => x.Trim()).ToList();
- var result = new Dictionary<string, int> {
- {"Start", fields.IndexOf("Start")},
- {"End", fields.IndexOf("End")},
- {"Text", fields.IndexOf("Text")}
- };
- return result;
+ return new Dictionary<string, int>
+ {
+ { "Start", fields.IndexOf("Start") },
+ { "End", fields.IndexOf("End") },
+ { "Text", fields.IndexOf("Text") }
+ };
}
- /// <summary>
- /// Credit: https://github.com/SubtitleEdit/subtitleedit/blob/master/src/Logic/SubtitleFormats/AdvancedSubStationAlpha.cs
- /// </summary>
private void RemoteNativeFormatting(SubtitleTrackEvent p)
{
int indexOfBegin = p.Text.IndexOf('{');
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
index f0d107196..c0023ebf2 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.IO;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
index bf8808eb8..dca5c1e8a 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
@@ -1,6 +1,9 @@
+#nullable enable
+#pragma warning disable CS1591
+
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class ParserValues
+ public static class ParserValues
{
public const string NewLine = "\r\n";
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
index 728efa788..cc35efb3f 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -20,6 +22,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_logger = logger;
}
+ /// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
@@ -55,11 +58,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
subEvent.StartPositionTicks = GetTicks(time[0]);
- var endTime = time[1];
- var idx = endTime.IndexOf(" ", StringComparison.Ordinal);
+ var endTime = time[1].AsSpan();
+ var idx = endTime.IndexOf(' ');
if (idx > 0)
{
- endTime = endTime.Substring(0, idx);
+ endTime = endTime.Slice(0, idx);
}
subEvent.EndPositionTicks = GetTicks(endTime);
@@ -88,7 +91,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return trackInfo;
}
- long GetTicks(string time)
+ private long GetTicks(ReadOnlySpan<char> time)
{
return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out var span)
? span.Ticks
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
index 45b317b2e..143c010b7 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
@@ -8,8 +8,12 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
+ /// <summary>
+ /// SRT subtitle writer.
+ /// </summary>
public class SrtWriter : ISubtitleWriter
{
+ /// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
index 77033c6b4..6b7a81e6e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -8,10 +8,11 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
- /// Credit to https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs
+ /// <see href="https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs">Credit</see>.
/// </summary>
public class SsaParser : ISubtitleParser
{
+ /// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
@@ -45,7 +46,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
header.AppendLine(line);
}
- if (line.Trim().ToLowerInvariant() == "[events]")
+ if (string.Equals(line.Trim(), "[events]", StringComparison.OrdinalIgnoreCase))
{
eventsStarted = true;
}
@@ -63,27 +64,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles
format = line.ToLowerInvariant().Substring(8).Split(',');
for (int i = 0; i < format.Length; i++)
{
- if (format[i].Trim().ToLowerInvariant() == "layer")
+ if (string.Equals(format[i].Trim(), "layer", StringComparison.OrdinalIgnoreCase))
{
indexLayer = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "start")
+ else if (string.Equals(format[i].Trim(), "start", StringComparison.OrdinalIgnoreCase))
{
indexStart = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "end")
+ else if (string.Equals(format[i].Trim(), "end", StringComparison.OrdinalIgnoreCase))
{
indexEnd = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "text")
+ else if (string.Equals(format[i].Trim(), "text", StringComparison.OrdinalIgnoreCase))
{
indexText = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "effect")
+ else if (string.Equals(format[i].Trim(), "effect", StringComparison.OrdinalIgnoreCase))
{
indexEffect = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "style")
+ else if (string.Equals(format[i].Trim(), "style", StringComparison.OrdinalIgnoreCase))
{
indexStyle = i;
}
@@ -178,18 +179,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
// h:mm:ss.cc
string[] timeCode = time.Split(':', '.');
- return new TimeSpan(0, int.Parse(timeCode[0]),
- int.Parse(timeCode[1]),
- int.Parse(timeCode[2]),
- int.Parse(timeCode[3]) * 10).Ticks;
+ return new TimeSpan(
+ 0,
+ int.Parse(timeCode[0]),
+ int.Parse(timeCode[1]),
+ int.Parse(timeCode[2]),
+ int.Parse(timeCode[3]) * 10).Ticks;
}
- public static string GetFormattedText(string text)
+ private static string GetFormattedText(string text)
{
text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
- bool italic = false;
-
for (int i = 0; i < 10; i++) // just look ten times...
{
if (text.Contains(@"{\fn"))
@@ -200,7 +201,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string fontName = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref fontName, ref extraTags, out italic);
+ CheckAndAddSubTags(ref fontName, ref extraTags, out bool italic);
text = text.Remove(start, end - start + 1);
if (italic)
{
@@ -231,7 +232,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string fontSize = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref fontSize, ref extraTags, out italic);
+ CheckAndAddSubTags(ref fontSize, ref extraTags, out bool italic);
if (IsInteger(fontSize))
{
text = text.Remove(start, end - start + 1);
@@ -265,7 +266,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string color = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref color, ref extraTags, out italic);
+ CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
color = color.Replace("&", string.Empty).TrimStart('H');
color = color.PadLeft(6, '0');
@@ -283,6 +284,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
}
+
int indexOfEndTag = text.IndexOf("{\\c}", start);
if (indexOfEndTag > 0)
{
@@ -303,7 +305,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string color = text.Substring(start + 5, end - (start + 5));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref color, ref extraTags, out italic);
+ CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
color = color.Replace("&", string.Empty).TrimStart('H');
color = color.PadLeft(6, '0');
@@ -321,6 +323,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
}
+
text += "</font>";
}
}
@@ -354,14 +357,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
private static bool IsInteger(string s)
- {
- if (int.TryParse(s, out var i))
- {
- return true;
- }
-
- return false;
- }
+ => int.TryParse(s, out _);
private static int CountTagInText(string text, string tag)
{
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index f1aa8ea5f..fbe8bd69f 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -32,6 +34,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private readonly IHttpClient _httpClient;
private readonly IMediaSourceManager _mediaSourceManager;
+ /// <summary>
+ /// The _semaphoreLocks.
+ /// </summary>
+ private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
+ new ConcurrentDictionary<string, SemaphoreSlim>();
+
public SubtitleEncoder(
ILibraryManager libraryManager,
ILogger<SubtitleEncoder> logger,
@@ -172,7 +180,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
inputFiles = new[] { mediaSource.Path };
}
- var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
+ var protocol = mediaSource.Protocol;
+ if (subtitleStream.IsExternal)
+ {
+ protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
+ }
+
+ var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
@@ -261,25 +275,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return new SubtitleInfo(subtitleStream.Path, protocol, currentFormat, true);
}
- private struct SubtitleInfo
- {
- public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
- {
- Path = path;
- Protocol = protocol;
- Format = format;
- IsExternal = isExternal;
- }
-
- public string Path { get; set; }
-
- public MediaProtocol Protocol { get; set; }
-
- public string Format { get; set; }
-
- public bool IsExternal { get; set; }
- }
-
private ISubtitleParser GetReader(string format, bool throwIfMissing)
{
if (string.IsNullOrEmpty(format))
@@ -353,25 +348,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
/// <summary>
- /// The _semaphoreLocks.
- /// </summary>
- private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
- new ConcurrentDictionary<string, SemaphoreSlim>();
-
- /// <summary>
/// Gets the lock.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.Object.</returns>
private SemaphoreSlim GetLock(string filename)
{
- return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
+ return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1));
}
/// <summary>
/// Converts the text subtitle to SRT.
/// </summary>
/// <param name="inputPath">The input path.</param>
+ /// <param name="language">The language.</param>
/// <param name="inputProtocol">The input protocol.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@@ -399,14 +389,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// Converts the text subtitle to SRT internal.
/// </summary>
/// <param name="inputPath">The input path.</param>
+ /// <param name="language">The language.</param>
/// <param name="inputProtocol">The input protocol.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">
- /// inputPath
- /// or
- /// outputPath
+ /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
/// </exception>
private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
{
@@ -426,9 +415,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
// FFmpeg automatically convert character encoding when it is UTF-16
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
- if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && (encodingParam == "UTF-16BE" || encodingParam == "UTF-16LE"))
+ if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) &&
+ (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
+ encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
{
- encodingParam = "";
+ encodingParam = string.Empty;
}
else if (!string.IsNullOrEmpty(encodingParam))
{
@@ -444,7 +435,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
CreateNoWindow = true,
UseShellExecute = false,
FileName = _mediaEncoder.EncoderPath,
- Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
+ Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
@@ -530,7 +521,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentException">Must use inputPath list overload</exception>
+ /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
private async Task ExtractTextSubtitle(
string[] inputFiles,
MediaProtocol protocol,
@@ -721,19 +712,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
- var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
+ ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
- var prefix = filename.Substring(0, 1);
+ var prefix = filename.Slice(0, 1);
- return Path.Combine(SubtitleCachePath, prefix, filename);
+ return Path.Join(SubtitleCachePath, prefix, filename);
}
else
{
- var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
+ ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
- var prefix = filename.Substring(0, 1);
+ var prefix = filename.Slice(0, 1);
- return Path.Combine(SubtitleCachePath, prefix, filename);
+ return Path.Join(SubtitleCachePath, prefix, filename);
}
}
@@ -749,7 +740,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
&& (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase)))
{
- charset = "";
+ charset = string.Empty;
}
_logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path);
@@ -780,5 +771,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentOutOfRangeException(nameof(protocol));
}
}
+
+ private struct SubtitleInfo
+ {
+ public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
+ {
+ Path = path;
+ Protocol = protocol;
+ Format = format;
+ IsExternal = isExternal;
+ }
+
+ public string Path { get; set; }
+
+ public MediaProtocol Protocol { get; set; }
+
+ public string Format { get; set; }
+
+ public bool IsExternal { get; set; }
+ }
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
index 7d3e18578..e5c785bc5 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
@@ -6,8 +6,12 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
+ /// <summary>
+ /// TTML subtitle writer.
+ /// </summary>
public class TtmlWriter : ISubtitleWriter
{
+ /// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
// Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml
@@ -36,9 +40,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase);
- writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
+ writer.WriteLine(
+ "<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
trackEvent.StartPositionTicks,
- (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks),
+ trackEvent.EndPositionTicks - trackEvent.StartPositionTicks,
text);
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index de35acbba..ad32cb794 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
writer.WriteLine(text);
- writer.WriteLine(string.Empty);
+ writer.WriteLine();
}
}
}
diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
index 66f3e1a94..b00d2fffb 100644
--- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
@@ -52,7 +52,13 @@ namespace MediaBrowser.Model.Configuration
public string PreviousVersionStr
{
get => PreviousVersion?.ToString();
- set => PreviousVersion = Version.Parse(value);
+ set
+ {
+ if (Version.TryParse(value, out var version))
+ {
+ PreviousVersion = version;
+ }
+ }
}
}
}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index a0c21a666..c66091f9d 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -255,6 +255,16 @@ namespace MediaBrowser.Model.Configuration
public string[] UninstalledPlugins { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
+ /// </summary>
+ public bool EnableSlowResponseWarning { get; set; }
+
+ /// <summary>
+ /// Gets or sets the threshold for the slow response time warning in ms.
+ /// </summary>
+ public long SlowResponseThresholdMs { get; set; }
+
+ /// <summary>
/// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
/// </summary>
public ServerConfiguration()
@@ -359,6 +369,9 @@ namespace MediaBrowser.Model.Configuration
DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" }
}
};
+
+ EnableSlowResponseWarning = true;
+ SlowResponseThresholdMs = 500;
}
}
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index a579f8464..93e60753a 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -157,7 +157,7 @@ namespace MediaBrowser.Model.Dlna
// flagValue = flagValue | DlnaFlags.TimeBasedSeek;
//}
- string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}",
+ string dlnaflags = string.Format(CultureInfo.InvariantCulture, ";DLNA.ORG_FLAGS={0}",
DlnaMaps.FlagsToString(flagValue));
ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(container,
diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs
index 052b4b78b..95cd0ac27 100644
--- a/MediaBrowser.Model/Dlna/DlnaMaps.cs
+++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs
@@ -1,18 +1,20 @@
#pragma warning disable CS1591
+using System.Globalization;
+
namespace MediaBrowser.Model.Dlna
{
public static class DlnaMaps
{
private static readonly string DefaultStreaming =
- FlagsToString(DlnaFlags.StreamingTransferMode |
+ FlagsToString(DlnaFlags.StreamingTransferMode |
DlnaFlags.BackgroundTransferMode |
DlnaFlags.ConnectionStall |
DlnaFlags.ByteBasedSeek |
DlnaFlags.DlnaV15);
private static readonly string DefaultInteractive =
- FlagsToString(DlnaFlags.InteractiveTransferMode |
+ FlagsToString(DlnaFlags.InteractiveTransferMode |
DlnaFlags.BackgroundTransferMode |
DlnaFlags.ConnectionStall |
DlnaFlags.ByteBasedSeek |
@@ -20,7 +22,7 @@ namespace MediaBrowser.Model.Dlna
public static string FlagsToString(DlnaFlags flags)
{
- return string.Format("{0:X8}{1:D24}", (ulong)flags, 0);
+ return string.Format(CultureInfo.InvariantCulture, "{0:X8}{1:D24}", (ulong)flags, 0);
}
public static string GetOrgOpValue(bool hasKnownRuntime, bool isDirectStream, TranscodeSeekInfo profileTranscodeSeekInfo)
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
index 398c5db8c..3c955989a 100644
--- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using MediaBrowser.Model.MediaInfo;
@@ -142,26 +143,26 @@ namespace MediaBrowser.Model.Dlna
{
if (timestampType == TransportStreamTimestamp.None)
{
- return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_ISO", resolution)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_ISO", resolution)) };
}
- return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) };
}
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
{
- return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) };
}
if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
{
- return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) };
}
if (string.IsNullOrEmpty(audioCodec) ||
string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
{
- return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) };
}
}
else if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase))
@@ -180,33 +181,33 @@ namespace MediaBrowser.Model.Dlna
{
suffix = string.Equals(suffix, "_ISO", StringComparison.OrdinalIgnoreCase) ? suffix : "_T";
- return new MediaFormatProfile[] { ValueOf(string.Format("VC1_TS_HD_DTS{0}", suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "VC1_TS_HD_DTS{0}", suffix)) };
}
}
else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
{
- return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AAC{0}", suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AAC{0}", suffix)) };
}
if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
{
- return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) };
}
if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase))
{
- return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) };
}
if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
{
- return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AC3{0}", suffix)) };
+ return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AC3{0}", suffix)) };
}
}
- return new MediaFormatProfile[] { };
+ return Array.Empty<MediaFormatProfile>();
}
private MediaFormatProfile ValueOf(string value)
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
index 1f7fa76ad..53e4540cb 100644
--- a/MediaBrowser.Model/Dlna/SortCriteria.cs
+++ b/MediaBrowser.Model/Dlna/SortCriteria.cs
@@ -1,6 +1,6 @@
#pragma warning disable CS1591
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Dlna
{
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index b89e9ce90..94d53ab70 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -144,7 +144,7 @@ namespace MediaBrowser.Model.Dlna
public Dictionary<string, string> StreamOptions { get; private set; }
- public string MediaSourceId => MediaSource == null ? null : MediaSource.Id;
+ public string MediaSourceId => MediaSource?.Id;
public bool IsDirectStream =>
PlayMethod == PlayMethod.DirectStream ||
@@ -191,7 +191,7 @@ namespace MediaBrowser.Model.Dlna
var encodedValue = pair.Value.Replace(" ", "%20");
- list.Add(string.Format("{0}={1}", pair.Name, encodedValue));
+ list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
}
string queryString = string.Join("&", list.ToArray());
@@ -214,18 +214,18 @@ namespace MediaBrowser.Model.Dlna
{
if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
- return string.Format("{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
- return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}
if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
- return string.Format("{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
- return string.Format("{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}
private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
@@ -457,7 +457,7 @@ namespace MediaBrowser.Model.Dlna
{
if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
{
- info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+ info.Url = string.Format(CultureInfo.InvariantCulture, "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
baseUrl,
ItemId,
MediaSourceId,
@@ -813,18 +813,18 @@ namespace MediaBrowser.Model.Dlna
{
var stream = TargetAudioStream;
- string inputCodec = stream == null ? null : stream.Codec;
+ string inputCodec = stream?.Codec;
if (IsDirectStream)
{
- return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec };
+ return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
}
foreach (string codec in AudioCodecs)
{
if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
{
- return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec };
+ return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
}
}
@@ -838,18 +838,18 @@ namespace MediaBrowser.Model.Dlna
{
var stream = TargetVideoStream;
- string inputCodec = stream == null ? null : stream.Codec;
+ string inputCodec = stream?.Codec;
if (IsDirectStream)
{
- return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec };
+ return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
}
foreach (string codec in VideoCodecs)
{
if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
{
- return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec };
+ return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
}
}
diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
index 7e5c5be3b..1f7fe3030 100644
--- a/MediaBrowser.Model/Entities/DisplayPreferences.cs
+++ b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
@@ -1,22 +1,18 @@
#nullable disable
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Entities
{
/// <summary>
/// Defines the display preferences for any item that supports them (usually Folders).
/// </summary>
- public class DisplayPreferences
+ public class DisplayPreferencesDto
{
/// <summary>
- /// The image scale.
+ /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
/// </summary>
- private const double ImageScale = .9;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DisplayPreferences" /> class.
- /// </summary>
- public DisplayPreferences()
+ public DisplayPreferencesDto()
{
RememberIndexing = false;
PrimaryImageHeight = 250;
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 7a488005e..f9ec0d238 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -112,107 +112,146 @@ namespace MediaBrowser.Model.Entities
{
get
{
- if (Type == MediaStreamType.Audio)
+ switch (Type)
{
- // if (!string.IsNullOrEmpty(Title))
- //{
- // return AddLanguageIfNeeded(Title);
- //}
-
- var attributes = new List<string>();
-
- if (!string.IsNullOrEmpty(Language))
- {
- attributes.Add(StringHelper.FirstToUpper(Language));
- }
-
- if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
- {
- attributes.Add(AudioCodec.GetFriendlyName(Codec));
- }
- else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
- {
- attributes.Add(Profile);
- }
-
- if (!string.IsNullOrEmpty(ChannelLayout))
- {
- attributes.Add(ChannelLayout);
- }
- else if (Channels.HasValue)
+ case MediaStreamType.Audio:
{
- attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
+ var attributes = new List<string>();
+
+ if (!string.IsNullOrEmpty(Language))
+ {
+ // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
+ string fullLanguage = CultureInfo
+ .GetCultures(CultureTypes.NeutralCultures)
+ .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
+ ?.DisplayName;
+ attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
+ }
+
+ if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
+ {
+ attributes.Add(AudioCodec.GetFriendlyName(Codec));
+ }
+ else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
+ {
+ attributes.Add(Profile);
+ }
+
+ if (!string.IsNullOrEmpty(ChannelLayout))
+ {
+ attributes.Add(StringHelper.FirstToUpper(ChannelLayout));
+ }
+ else if (Channels.HasValue)
+ {
+ attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
+ }
+
+ if (IsDefault)
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+ }
+
+ if (!string.IsNullOrEmpty(Title))
+ {
+ var result = new StringBuilder(Title);
+ foreach (var tag in attributes)
+ {
+ // Keep Tags that are not already in Title.
+ if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ result.Append(" - ").Append(tag);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ return string.Join(" - ", attributes);
}
- if (IsDefault)
+ case MediaStreamType.Video:
{
- attributes.Add("Default");
+ var attributes = new List<string>();
+
+ var resolutionText = GetResolutionText();
+
+ if (!string.IsNullOrEmpty(resolutionText))
+ {
+ attributes.Add(resolutionText);
+ }
+
+ if (!string.IsNullOrEmpty(Codec))
+ {
+ attributes.Add(Codec.ToUpperInvariant());
+ }
+
+ if (!string.IsNullOrEmpty(Title))
+ {
+ var result = new StringBuilder(Title);
+ foreach (var tag in attributes)
+ {
+ // Keep Tags that are not already in Title.
+ if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ result.Append(" - ").Append(tag);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ return string.Join(" ", attributes);
}
- return string.Join(" ", attributes);
- }
-
- if (Type == MediaStreamType.Video)
- {
- var attributes = new List<string>();
-
- var resolutionText = GetResolutionText();
-
- if (!string.IsNullOrEmpty(resolutionText))
+ case MediaStreamType.Subtitle:
{
- attributes.Add(resolutionText);
+ var attributes = new List<string>();
+
+ if (!string.IsNullOrEmpty(Language))
+ {
+ // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
+ string fullLanguage = CultureInfo
+ .GetCultures(CultureTypes.NeutralCultures)
+ .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
+ ?.DisplayName;
+ attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
+ }
+ else
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
+ }
+
+ if (IsDefault)
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+ }
+
+ if (IsForced)
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
+ }
+
+ if (!string.IsNullOrEmpty(Title))
+ {
+ var result = new StringBuilder(Title);
+ foreach (var tag in attributes)
+ {
+ // Keep Tags that are not already in Title.
+ if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ result.Append(" - ").Append(tag);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ return string.Join(" - ", attributes);
}
- if (!string.IsNullOrEmpty(Codec))
- {
- attributes.Add(Codec.ToUpperInvariant());
- }
-
- return string.Join(" ", attributes);
+ default:
+ return null;
}
-
- if (Type == MediaStreamType.Subtitle)
- {
-
- var attributes = new List<string>();
-
- if (!string.IsNullOrEmpty(Language))
- {
- attributes.Add(StringHelper.FirstToUpper(Language));
- }
- else
- {
- attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
- }
-
- if (IsDefault)
- {
- attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
- }
-
- if (IsForced)
- {
- attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
- }
-
- if (!string.IsNullOrEmpty(Title))
- {
- return attributes.AsEnumerable()
- // keep Tags that are not already in Title
- .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
- // attributes concatenation, starting with Title
- .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr))
- .ToString();
- }
-
- return string.Join(" - ", attributes.ToArray());
- }
-
- if (Type == MediaStreamType.Video)
- {
- }
-
- return null;
}
}
diff --git a/MediaBrowser.Model/Entities/ScrollDirection.cs b/MediaBrowser.Model/Entities/ScrollDirection.cs
deleted file mode 100644
index a1de0edcb..000000000
--- a/MediaBrowser.Model/Entities/ScrollDirection.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
- /// <summary>
- /// Enum ScrollDirection.
- /// </summary>
- public enum ScrollDirection
- {
- /// <summary>
- /// The horizontal.
- /// </summary>
- Horizontal,
-
- /// <summary>
- /// The vertical.
- /// </summary>
- Vertical
- }
-}
diff --git a/MediaBrowser.Model/Entities/SortOrder.cs b/MediaBrowser.Model/Entities/SortOrder.cs
deleted file mode 100644
index f3abc06f3..000000000
--- a/MediaBrowser.Model/Entities/SortOrder.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
- /// <summary>
- /// Enum SortOrder.
- /// </summary>
- public enum SortOrder
- {
- /// <summary>
- /// The ascending.
- /// </summary>
- Ascending,
-
- /// <summary>
- /// The descending.
- /// </summary>
- Descending
- }
-}
diff --git a/MediaBrowser.Model/Extensions/ListHelper.cs b/MediaBrowser.Model/Extensions/ListHelper.cs
deleted file mode 100644
index b893a3509..000000000
--- a/MediaBrowser.Model/Extensions/ListHelper.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Extensions
-{
- // TODO: @bond remove
- public static class ListHelper
- {
- public static bool ContainsIgnoreCase(string[] list, string value)
- {
- if (value == null)
- {
- throw new ArgumentNullException(nameof(value));
- }
-
- foreach (var item in list)
- {
- if (string.Equals(item, value, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
- }
-}
diff --git a/MediaBrowser.Model/IO/IODefaults.cs b/MediaBrowser.Model/IO/IODefaults.cs
index f392dbcce..d9a1e6777 100644
--- a/MediaBrowser.Model/IO/IODefaults.cs
+++ b/MediaBrowser.Model/IO/IODefaults.cs
@@ -1,3 +1,5 @@
+using System.IO;
+
namespace MediaBrowser.Model.IO
{
/// <summary>
@@ -14,5 +16,10 @@ namespace MediaBrowser.Model.IO
/// The default file stream buffer size.
/// </summary>
public const int FileStreamBufferSize = 4096;
+
+ /// <summary>
+ /// The default <see cref="StreamWriter" /> buffer size.
+ /// </summary>
+ public const int StreamWriterBufferSize = 1024;
}
}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
index 2b2377fda..ab74aff28 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -2,7 +2,7 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.LiveTv
{
diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
index b899a464b..dae885775 100644
--- a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
+++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
@@ -1,6 +1,6 @@
#pragma warning disable CS1591
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.LiveTv
{
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 83bd0c07e..902e29b20 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -23,7 +23,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup>
diff --git a/MediaBrowser.Model/Notifications/NotificationOption.cs b/MediaBrowser.Model/Notifications/NotificationOption.cs
index ea363d9b1..58aecb3d3 100644
--- a/MediaBrowser.Model/Notifications/NotificationOption.cs
+++ b/MediaBrowser.Model/Notifications/NotificationOption.cs
@@ -1,3 +1,4 @@
+#pragma warning disable CA1819 // Properties should not return arrays
#pragma warning disable CS1591
using System;
@@ -9,21 +10,27 @@ namespace MediaBrowser.Model.Notifications
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; }
+ public string? Type { get; set; }
/// <summary>
- /// User Ids to not monitor (it's opt out).
+ /// Gets or sets user Ids to not monitor (it's opt out).
/// </summary>
public string[] DisabledMonitorUsers { get; set; }
/// <summary>
- /// User Ids to send to (if SendToUserMode == Custom)
+ /// Gets or sets user Ids to send to (if SendToUserMode == Custom).
/// </summary>
public string[] SendToUsers { get; set; }
diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
index 12d537492..2ef6f7c60 100644
--- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
+++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
@@ -1,6 +1,7 @@
#nullable disable
#pragma warning disable CS1591
+using System;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Querying
@@ -54,7 +55,7 @@ namespace MediaBrowser.Model.Querying
public UpcomingEpisodesQuery()
{
- EnableImageTypes = new ImageType[] { };
+ EnableImageTypes = Array.Empty<ImageType>();
}
}
}
diff --git a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs
index d67b6bd55..0c77a6132 100644
--- a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs
+++ b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs
@@ -12,11 +12,5 @@ namespace MediaBrowser.Model.SyncPlay
/// </summary>
/// <value>The Group id to join.</value>
public Guid GroupId { get; set; }
-
- /// <summary>
- /// Gets or sets the playing item id.
- /// </summary>
- /// <value>The client's currently playing item id.</value>
- public Guid PlayingItemId { get; set; }
}
}
diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
index 671f4e01f..e89efeed8 100644
--- a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
+++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
@@ -23,16 +23,16 @@ namespace MediaBrowser.Model.SyncPlay
/// <summary>
/// A user is signaling that playback is buffering.
/// </summary>
- Buffering = 3,
+ Buffer = 3,
/// <summary>
/// A user is signaling that playback resumed.
/// </summary>
- BufferingDone = 4,
+ Ready = 4,
/// <summary>
/// A user is reporting its ping.
/// </summary>
- UpdatePing = 5
+ Ping = 5
}
}
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index f655b8edd..413d297cb 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -124,13 +124,16 @@ namespace MediaBrowser.Providers.Manager
var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
// If there are more than one output paths, the stream will need to be seekable
- var memoryStream = new MemoryStream();
- using (source)
+ if (paths.Length > 1 && !source.CanSeek)
{
- await source.CopyToAsync(memoryStream).ConfigureAwait(false);
- }
+ var memoryStream = new MemoryStream();
+ await using (source.ConfigureAwait(false))
+ {
+ await source.CopyToAsync(memoryStream).ConfigureAwait(false);
+ }
- source = memoryStream;
+ source = memoryStream;
+ }
var currentImage = GetCurrentImage(item, type, index);
var currentImageIsLocalFile = currentImage != null && currentImage.IsLocalFile;
@@ -138,22 +141,23 @@ namespace MediaBrowser.Providers.Manager
var savedPaths = new List<string>();
- await using (source)
+ await using (source.ConfigureAwait(false))
{
- var currentPathIndex = 0;
-
- foreach (var path in paths)
+ for (int i = 0; i < paths.Length; i++)
{
- source.Position = 0;
+ if (i != 0)
+ {
+ source.Position = 0;
+ }
+
string retryPath = null;
if (paths.Length == retryPaths.Length)
{
- retryPath = retryPaths[currentPathIndex];
+ retryPath = retryPaths[i];
}
- var savedPath = await SaveImageToLocation(source, path, retryPath, cancellationToken).ConfigureAwait(false);
+ var savedPath = await SaveImageToLocation(source, paths[i], retryPath, cancellationToken).ConfigureAwait(false);
savedPaths.Add(savedPath);
- currentPathIndex++;
}
}
@@ -183,7 +187,7 @@ namespace MediaBrowser.Providers.Manager
}
}
- public async Task SaveImage(User user, Stream source, string path)
+ public async Task SaveImage(Stream source, string path)
{
await SaveImageToLocation(source, path, path, CancellationToken.None).ConfigureAwait(false);
}
@@ -224,7 +228,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- source.Position = 0;
await SaveImageToLocation(source, retryPath, cancellationToken).ConfigureAwait(false);
return retryPath;
}
@@ -253,7 +256,7 @@ namespace MediaBrowser.Providers.Manager
await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
- await source.CopyToAsync(fs, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
if (_config.Configuration.SaveMetadataHidden)
@@ -352,7 +355,7 @@ namespace MediaBrowser.Providers.Manager
if (string.IsNullOrWhiteSpace(extension))
{
- throw new ArgumentException(string.Format("Unable to determine image file extension from mime type {0}", mimeType));
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
}
if (type == ImageType.Thumb && saveLocally)
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 6cc3ca369..9227b6d93 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -54,7 +54,12 @@ namespace MediaBrowser.Providers.Manager
return hasChanges;
}
- public async Task<RefreshResult> RefreshImages(BaseItem item, LibraryOptions libraryOptions, List<IImageProvider> providers, ImageRefreshOptions refreshOptions, MetadataOptions savedOptions, CancellationToken cancellationToken)
+ public async Task<RefreshResult> RefreshImages(
+ BaseItem item,
+ LibraryOptions libraryOptions,
+ List<IImageProvider> providers,
+ ImageRefreshOptions refreshOptions,
+ CancellationToken cancellationToken)
{
if (refreshOptions.IsReplacingImage(ImageType.Backdrop))
{
@@ -78,19 +83,15 @@ namespace MediaBrowser.Providers.Manager
foreach (var provider in providers)
{
- var remoteProvider = provider as IRemoteImageProvider;
-
- if (remoteProvider != null)
+ if (provider is IRemoteImageProvider remoteProvider)
{
await RefreshFromProvider(item, libraryOptions, remoteProvider, refreshOptions, typeOptions, backdropLimit, screenshotLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false);
continue;
}
- var dynamicImageProvider = provider as IDynamicImageProvider;
-
- if (dynamicImageProvider != null)
+ if (provider is IDynamicImageProvider dynamicImageProvider)
{
- await RefreshFromProvider(item, dynamicImageProvider, refreshOptions, typeOptions, libraryOptions, downloadedImages, result, cancellationToken).ConfigureAwait(false);
+ await RefreshFromProvider(item, dynamicImageProvider, refreshOptions, typeOptions, downloadedImages, result, cancellationToken).ConfigureAwait(false);
}
}
@@ -100,11 +101,11 @@ namespace MediaBrowser.Providers.Manager
/// <summary>
/// Refreshes from provider.
/// </summary>
- private async Task RefreshFromProvider(BaseItem item,
+ private async Task RefreshFromProvider(
+ BaseItem item,
IDynamicImageProvider provider,
ImageRefreshOptions refreshOptions,
TypeOptions savedOptions,
- LibraryOptions libraryOptions,
ICollection<ImageType> downloadedImages,
RefreshResult result,
CancellationToken cancellationToken)
@@ -115,7 +116,7 @@ namespace MediaBrowser.Providers.Manager
foreach (var imageType in images)
{
- if (!IsEnabled(savedOptions, imageType, item))
+ if (!IsEnabled(savedOptions, imageType))
{
continue;
}
@@ -133,12 +134,13 @@ namespace MediaBrowser.Providers.Manager
if (response.Protocol == MediaProtocol.Http)
{
_logger.LogDebug("Setting image url into item {0}", item.Id);
- item.SetImage(new ItemImageInfo
- {
- Path = response.Path,
- Type = imageType
-
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = response.Path,
+ Type = imageType
+ },
+ 0);
}
else
{
@@ -157,7 +159,7 @@ namespace MediaBrowser.Providers.Manager
}
downloadedImages.Add(imageType);
- result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
+ result.UpdateType |= ItemUpdateType.ImageUpdate;
}
}
}
@@ -279,7 +281,7 @@ namespace MediaBrowser.Providers.Manager
foreach (var imageType in _singularImages)
{
- if (!IsEnabled(savedOptions, imageType, item))
+ if (!IsEnabled(savedOptions, imageType))
{
continue;
}
@@ -299,8 +301,7 @@ namespace MediaBrowser.Providers.Manager
minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
await DownloadBackdrops(item, libraryOptions, ImageType.Backdrop, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
- var hasScreenshots = item as IHasScreenshots;
- if (hasScreenshots != null)
+ if (item is IHasScreenshots hasScreenshots)
{
minWidth = savedOptions.GetMinWidth(ImageType.Screenshot);
await DownloadBackdrops(item, libraryOptions, ImageType.Screenshot, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
@@ -317,7 +318,7 @@ namespace MediaBrowser.Providers.Manager
}
}
- private bool IsEnabled(TypeOptions options, ImageType type, BaseItem item)
+ private bool IsEnabled(TypeOptions options, ImageType type)
{
return options.IsEnabled(type);
}
@@ -452,10 +453,10 @@ namespace MediaBrowser.Providers.Manager
.Where(i => i.Type == type && !(i.Width.HasValue && i.Width.Value < minWidth))
.ToList();
- if (EnableImageStub(item, type, libraryOptions) && eligibleImages.Count > 0)
+ if (EnableImageStub(item, libraryOptions) && eligibleImages.Count > 0)
{
SaveImageStub(item, type, eligibleImages.Select(i => i.Url));
- result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
+ result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
}
@@ -465,11 +466,18 @@ namespace MediaBrowser.Providers.Manager
try
{
- var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
-
- await _providerManager.SaveImage(item, response.Content, response.ContentType, type, null, cancellationToken).ConfigureAwait(false);
-
- result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
+ using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+ await _providerManager.SaveImage(
+ item,
+ stream,
+ response.Content.Headers.ContentType.MediaType,
+ type,
+ null,
+ cancellationToken).ConfigureAwait(false);
+
+ result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
}
catch (HttpException ex)
@@ -488,7 +496,7 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- private bool EnableImageStub(BaseItem item, ImageType type, LibraryOptions libraryOptions)
+ private bool EnableImageStub(BaseItem item, LibraryOptions libraryOptions)
{
if (item is LiveTvProgram)
{
@@ -556,23 +564,23 @@ namespace MediaBrowser.Providers.Manager
var url = image.Url;
- if (EnableImageStub(item, imageType, libraryOptions))
+ if (EnableImageStub(item, libraryOptions))
{
SaveImageStub(item, imageType, new[] { url });
- result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
+ result.UpdateType |= ItemUpdateType.ImageUpdate;
continue;
}
try
{
- var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+ using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
// If there's already an image of the same size, skip it
- if (response.ContentLength.HasValue)
+ if (response.Content.Headers.ContentLength.HasValue)
{
try
{
- if (item.GetImages(imageType).Any(i => _fileSystem.GetFileInfo(i.Path).Length == response.ContentLength.Value))
+ if (item.GetImages(imageType).Any(i => _fileSystem.GetFileInfo(i.Path).Length == response.Content.Headers.ContentLength.Value))
{
response.Content.Dispose();
continue;
@@ -584,7 +592,14 @@ namespace MediaBrowser.Providers.Manager
}
}
- await _providerManager.SaveImage(item, response.Content, response.ContentType, imageType, null, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await _providerManager.SaveImage(
+ item,
+ stream,
+ response.Content.Headers.ContentType.MediaType,
+ imageType,
+ null,
+ cancellationToken).ConfigureAwait(false);
result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
}
catch (HttpException ex)
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index ecfa3d46d..dcae300fc 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -52,7 +52,6 @@ namespace MediaBrowser.Providers.Manager
public async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
{
var itemOfType = (TItemType)item;
- var config = ProviderManager.GetMetadataOptions(item);
var updateType = ItemUpdateType.None;
var requiresRefresh = false;
@@ -86,7 +85,7 @@ namespace MediaBrowser.Providers.Manager
// Always validate images and check for new locally stored ones.
if (itemImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
{
- updateType = updateType | ItemUpdateType.ImageUpdate;
+ updateType |= ItemUpdateType.ImageUpdate;
}
}
catch (Exception ex)
@@ -102,7 +101,7 @@ namespace MediaBrowser.Providers.Manager
bool hasRefreshedMetadata = true;
bool hasRefreshedImages = true;
- var isFirstRefresh = item.DateLastRefreshed == default(DateTime);
+ var isFirstRefresh = item.DateLastRefreshed == default;
// Next run metadata providers
if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
@@ -114,7 +113,7 @@ namespace MediaBrowser.Providers.Manager
{
if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata))
{
- updateType = updateType | ItemUpdateType.MetadataImport;
+ updateType |= ItemUpdateType.MetadataImport;
}
}
@@ -132,7 +131,7 @@ namespace MediaBrowser.Providers.Manager
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false);
- updateType = updateType | result.UpdateType;
+ updateType |= result.UpdateType;
if (result.Failures > 0)
{
hasRefreshedMetadata = false;
@@ -147,9 +146,9 @@ namespace MediaBrowser.Providers.Manager
if (providers.Count > 0)
{
- var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, config, cancellationToken).ConfigureAwait(false);
+ var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
- updateType = updateType | result.UpdateType;
+ updateType |= result.UpdateType;
if (result.Failures > 0)
{
hasRefreshedImages = false;
@@ -158,7 +157,7 @@ namespace MediaBrowser.Providers.Manager
}
var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType);
- updateType = updateType | beforeSaveResult;
+ updateType |= beforeSaveResult;
// Save if changes were made, or it's never been saved before
if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
@@ -175,7 +174,7 @@ namespace MediaBrowser.Providers.Manager
// If any of these properties are set then make sure the updateType is not None, just to force everything to save
if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
{
- updateType = updateType | ItemUpdateType.MetadataDownload;
+ updateType |= ItemUpdateType.MetadataDownload;
}
if (hasRefreshedMetadata && hasRefreshedImages)
@@ -184,11 +183,11 @@ namespace MediaBrowser.Providers.Manager
}
else
{
- item.DateLastRefreshed = default(DateTime);
+ item.DateLastRefreshed = default;
}
// Save to database
- SaveItem(metadataResult, libraryOptions, updateType, cancellationToken);
+ await SaveItemAsync(metadataResult, libraryOptions, updateType, cancellationToken).ConfigureAwait(false);
}
await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
@@ -203,26 +202,26 @@ namespace MediaBrowser.Providers.Manager
lookupInfo.Year = result.ProductionYear;
}
- protected void SaveItem(MetadataResult<TItemType> result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken)
+ protected async Task SaveItemAsync(MetadataResult<TItemType> result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken)
{
if (result.Item.SupportsPeople && result.People != null)
{
var baseItem = result.Item;
LibraryManager.UpdatePeople(baseItem, result.People);
- SavePeopleMetadata(result.People, libraryOptions, cancellationToken);
+ await SavePeopleMetadataAsync(result.People, libraryOptions, cancellationToken).ConfigureAwait(false);
}
result.Item.UpdateToRepository(reason, cancellationToken);
}
- private void SavePeopleMetadata(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
+ private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
{
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
- if (person.ProviderIds.Any() || !string.IsNullOrWhiteSpace(person.ImageUrl))
+ if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
{
var updateType = ItemUpdateType.MetadataDownload;
@@ -239,10 +238,10 @@ namespace MediaBrowser.Providers.Manager
if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
{
- AddPersonImage(personEntity, libraryOptions, person.ImageUrl, cancellationToken);
+ await AddPersonImageAsync(personEntity, libraryOptions, person.ImageUrl, cancellationToken).ConfigureAwait(false);
saveEntity = true;
- updateType = updateType | ItemUpdateType.ImageUpdate;
+ updateType |= ItemUpdateType.ImageUpdate;
}
if (saveEntity)
@@ -253,26 +252,28 @@ namespace MediaBrowser.Providers.Manager
}
}
- private void AddPersonImage(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken)
+ private async Task AddPersonImageAsync(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken)
{
- // if (libraryOptions.DownloadImagesInAdvance)
- //{
- // try
- // {
- // await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
- // return;
- // }
- // catch (Exception ex)
- // {
- // Logger.LogError(ex, "Error in AddPersonImage");
- // }
- //}
-
- personEntity.SetImage(new ItemImageInfo
- {
- Path = imageUrl,
- Type = ImageType.Primary
- }, 0);
+ if (libraryOptions.DownloadImagesInAdvance)
+ {
+ try
+ {
+ await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error in AddPersonImage");
+ }
+ }
+
+ personEntity.SetImage(
+ new ItemImageInfo
+ {
+ Path = imageUrl,
+ Type = ImageType.Primary
+ },
+ 0);
}
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
@@ -723,7 +724,7 @@ namespace MediaBrowser.Providers.Manager
userDataList.AddRange(localItem.UserDataList);
}
- MergeData(localItem, temp, new MetadataField[] { }, !options.ReplaceAllMetadata, true);
+ MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport;
// Only one local provider allowed per item
@@ -849,7 +850,7 @@ namespace MediaBrowser.Providers.Manager
{
result.Provider = provider.Name;
- MergeData(result, temp, new MetadataField[] { }, false, false);
+ MergeData(result, temp, Array.Empty<MetadataField>(), false, false);
MergeNewData(temp.Item, id);
refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 86a182fe5..d9a84be5c 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1,11 +1,12 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -24,8 +25,8 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using Priority_Queue;
using Book = MediaBrowser.Controller.Entities.Book;
@@ -42,61 +43,76 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
public class ProviderManager : IProviderManager, IDisposable
{
+ private readonly object _refreshQueueLock = new object();
private readonly ILogger<ProviderManager> _logger;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryMonitor _libraryMonitor;
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
- private readonly IJsonSerializer _json;
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager;
private readonly IServerConfigurationManager _configurationManager;
+ private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>();
+ private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
+ private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
+ new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>();
- private IImageProvider[] ImageProviders { get; set; }
-
- private IMetadataService[] _metadataServices = { };
- private IMetadataProvider[] _metadataProviders = { };
+ private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
+ private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>();
private IEnumerable<IMetadataSaver> _savers;
-
private IExternalId[] _externalIds;
-
- private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
-
- public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
- public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
- public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
+ private bool _isProcessingRefreshQueue;
+ private bool _disposed;
/// <summary>
- /// Initializes a new instance of the <see cref="ProviderManager" /> class.
+ /// Initializes a new instance of the <see cref="ProviderManager"/> class.
/// </summary>
+ /// <param name="httpClientFactory">The Http client factory.</param>
+ /// <param name="subtitleManager">The subtitle manager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <param name="libraryMonitor">The library monitor.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="appPaths">The server application paths.</param>
+ /// <param name="libraryManager">The library manager.</param>
public ProviderManager(
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ISubtitleManager subtitleManager,
IServerConfigurationManager configurationManager,
ILibraryMonitor libraryMonitor,
ILogger<ProviderManager> logger,
IFileSystem fileSystem,
IServerApplicationPaths appPaths,
- ILibraryManager libraryManager,
- IJsonSerializer json)
+ ILibraryManager libraryManager)
{
_logger = logger;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_configurationManager = configurationManager;
_libraryMonitor = libraryMonitor;
_fileSystem = fileSystem;
_appPaths = appPaths;
_libraryManager = libraryManager;
- _json = json;
_subtitleManager = subtitleManager;
}
- /// <summary>
- /// Adds the metadata providers.
- /// </summary>
- public void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices,
- IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers,
- IEnumerable<IExternalId> externalIds)
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
+
+ private IImageProvider[] ImageProviders { get; set; }
+
+ /// <inheritdoc/>
+ public void AddParts(
+ IEnumerable<IImageProvider> imageProviders,
+ IEnumerable<IMetadataService> metadataServices,
+ IEnumerable<IMetadataProvider> metadataProviders,
+ IEnumerable<IMetadataSaver> metadataSavers,
+ IEnumerable<IExternalId> externalIds)
{
ImageProviders = imageProviders.ToArray();
@@ -104,27 +120,17 @@ namespace MediaBrowser.Providers.Manager
_metadataProviders = metadataProviders.ToArray();
_externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
- _savers = metadataSavers.Where(i =>
- {
- var configurable = i as IConfigurableProvider;
-
- return configurable == null || configurable.IsEnabled;
- }).ToArray();
+ _savers = metadataSavers
+ .Where(i => !(i is IConfigurableProvider configurable) || configurable.IsEnabled)
+ .ToArray();
}
+ /// <inheritdoc/>
public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
- IMetadataService service = null;
var type = item.GetType();
- foreach (var current in _metadataServices)
- {
- if (current.CanRefreshPrimary(type))
- {
- service = current;
- break;
- }
- }
+ var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type));
if (service == null)
{
@@ -147,35 +153,50 @@ namespace MediaBrowser.Providers.Manager
return Task.FromResult(ItemUpdateType.None);
}
+ /// <inheritdoc/>
public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
{
- using (var response = await _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url,
- BufferContent = false
+ var httpClient = _httpClientFactory.CreateClient();
+ using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+
+ var contentType = response.Content.Headers.ContentType.MediaType;
- }).ConfigureAwait(false))
+ // Workaround for tvheadend channel icons
+ // TODO: Isolate this hack into the tvh plugin
+ if (string.IsNullOrEmpty(contentType))
{
- // Workaround for tvheadend channel icons
- // TODO: Isolate this hack into the tvh plugin
- if (string.IsNullOrEmpty(response.ContentType))
+ if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1)
{
- if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- response.ContentType = "image/png";
- }
+ contentType = "image/png";
}
+ }
- await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
+ // thetvdb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
+ if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new HttpException("Invalid image received.")
+ {
+ StatusCode = HttpStatusCode.NotFound
+ };
}
+
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await SaveImage(
+ item,
+ stream,
+ contentType,
+ type,
+ imageIndex,
+ cancellationToken).ConfigureAwait(false);
}
+ /// <inheritdoc/>
public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
{
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken);
}
+ /// <inheritdoc/>
public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(source))
@@ -188,12 +209,14 @@ namespace MediaBrowser.Providers.Manager
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
}
- public Task SaveImage(User user, Stream source, string mimeType, string path)
+ /// <inheritdoc/>
+ public Task SaveImage(Stream source, string mimeType, string path)
{
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
- .SaveImage(user, source, path);
+ .SaveImage(source, path);
}
+ /// <inheritdoc/>
public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
{
var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
@@ -213,7 +236,7 @@ namespace MediaBrowser.Providers.Manager
languages.Add(preferredLanguage);
}
- var tasks = providers.Select(i => GetImages(item, cancellationToken, i, languages, query.ImageType));
+ var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -224,12 +247,17 @@ namespace MediaBrowser.Providers.Manager
/// Gets the images.
/// </summary>
/// <param name="item">The item.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="provider">The provider.</param>
/// <param name="preferredLanguages">The preferred languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="type">The type.</param>
/// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
- private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider provider, List<string> preferredLanguages, ImageType? type = null)
+ private async Task<IEnumerable<RemoteImageInfo>> GetImages(
+ BaseItem item,
+ IRemoteImageProvider provider,
+ IReadOnlyCollection<string> preferredLanguages,
+ CancellationToken cancellationToken,
+ ImageType? type = null)
{
try
{
@@ -255,21 +283,23 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- _logger.LogError(ex, "{0} failed in GetImageInfos for type {1}", provider.GetType().Name, item.GetType().Name);
+ _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
return new List<RemoteImageInfo>();
}
}
- /// <summary>
- /// Gets the supported image providers.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>IEnumerable{IImageProvider}.</returns>
+ /// <inheritdoc/>
public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item)
{
return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(item).ToArray()));
}
+ /// <summary>
+ /// Gets the image providers for the provided item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="refreshOptions">The image refresh options.</param>
+ /// <returns>The image providers for the item.</returns>
public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions)
{
return GetImageProviders(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false);
@@ -283,7 +313,7 @@ namespace MediaBrowser.Providers.Manager
var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
var typeFetcherOrder = typeOptions?.ImageFetcherOrder;
- return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, options, refreshOptions, includeDisabled))
+ return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, refreshOptions, includeDisabled))
.OrderBy(i =>
{
// See if there's a user-defined order
@@ -304,6 +334,13 @@ namespace MediaBrowser.Providers.Manager
.ThenBy(GetOrder);
}
+ /// <summary>
+ /// Gets the metadata providers for the provided item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <typeparam name="T">The type of metadata provider.</typeparam>
+ /// <returns>The metadata providers.</returns>
public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
where T : BaseItem
{
@@ -319,7 +356,7 @@ namespace MediaBrowser.Providers.Manager
var currentOptions = globalMetadataOptions;
return _metadataProviders.OfType<IMetadataProvider<T>>()
- .Where(i => CanRefresh(i, item, libraryOptions, currentOptions, includeDisabled, forceEnableInternetMetadata))
+ .Where(i => CanRefresh(i, item, libraryOptions, includeDisabled, forceEnableInternetMetadata))
.OrderBy(i => GetConfiguredOrder(item, i, libraryOptions, globalMetadataOptions))
.ThenBy(GetDefaultOrder);
}
@@ -329,14 +366,20 @@ namespace MediaBrowser.Providers.Manager
var options = GetMetadataOptions(item);
var libraryOptions = _libraryManager.GetLibraryOptions(item);
- return GetImageProviders(item, libraryOptions, options,
- new ImageRefreshOptions(
- new DirectoryService(_fileSystem)),
- includeDisabled)
- .OfType<IRemoteImageProvider>();
+ return GetImageProviders(
+ item,
+ libraryOptions,
+ options,
+ new ImageRefreshOptions(new DirectoryService(_fileSystem)),
+ includeDisabled).OfType<IRemoteImageProvider>();
}
- private bool CanRefresh(IMetadataProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, bool includeDisabled, bool forceEnableInternetMetadata)
+ private bool CanRefresh(
+ IMetadataProvider provider,
+ BaseItem item,
+ LibraryOptions libraryOptions,
+ bool includeDisabled,
+ bool forceEnableInternetMetadata)
{
if (!includeDisabled)
{
@@ -372,7 +415,12 @@ namespace MediaBrowser.Providers.Manager
return true;
}
- private bool CanRefresh(IImageProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled)
+ private bool CanRefresh(
+ IImageProvider provider,
+ BaseItem item,
+ LibraryOptions libraryOptions,
+ ImageRefreshOptions refreshOptions,
+ bool includeDisabled)
{
if (!includeDisabled)
{
@@ -400,7 +448,7 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- _logger.LogError(ex, "{0} failed in Supports for type {1}", provider.GetType().Name, item.GetType().Name);
+ _logger.LogError(ex, "{ProviderName} failed in Supports for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
return false;
}
}
@@ -412,9 +460,7 @@ namespace MediaBrowser.Providers.Manager
/// <returns>System.Int32.</returns>
private int GetOrder(IImageProvider provider)
{
- var hasOrder = provider as IHasOrder;
-
- if (hasOrder == null)
+ if (!(provider is IHasOrder hasOrder))
{
return 0;
}
@@ -441,7 +487,7 @@ namespace MediaBrowser.Providers.Manager
if (provider is IRemoteMetadataProvider)
{
var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
- var typeFetcherOrder = typeOptions == null ? null : typeOptions.MetadataFetcherOrder;
+ var typeFetcherOrder = typeOptions?.MetadataFetcherOrder;
var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
@@ -459,9 +505,7 @@ namespace MediaBrowser.Providers.Manager
private int GetDefaultOrder(IMetadataProvider provider)
{
- var hasOrder = provider as IHasOrder;
-
- if (hasOrder != null)
+ if (provider is IHasOrder hasOrder)
{
return hasOrder.Order;
}
@@ -469,9 +513,10 @@ namespace MediaBrowser.Providers.Manager
return 0;
}
+ /// <inheritdoc/>
public MetadataPluginSummary[] GetAllMetadataPlugins()
{
- return new MetadataPluginSummary[]
+ return new[]
{
GetPluginSummary<Movie>(),
GetPluginSummary<BoxSet>(),
@@ -493,7 +538,7 @@ namespace MediaBrowser.Providers.Manager
where T : BaseItem, new()
{
// Give it a dummy path just so that it looks like a file system item
- var dummy = new T()
+ var dummy = new T
{
Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
ParentId = Guid.NewGuid()
@@ -508,16 +553,17 @@ namespace MediaBrowser.Providers.Manager
var libraryOptions = new LibraryOptions();
- var imageProviders = GetImageProviders(dummy, libraryOptions, options,
- new ImageRefreshOptions(
- new DirectoryService(_fileSystem)),
- true)
- .ToList();
+ var imageProviders = GetImageProviders(
+ dummy,
+ libraryOptions,
+ options,
+ new ImageRefreshOptions(new DirectoryService(_fileSystem)),
+ true).ToList();
var pluginList = summary.Plugins.ToList();
AddMetadataPlugins(pluginList, dummy, libraryOptions, options);
- AddImagePlugins(pluginList, dummy, imageProviders);
+ AddImagePlugins(pluginList, imageProviders);
var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
@@ -548,14 +594,14 @@ namespace MediaBrowser.Providers.Manager
var providers = GetMetadataProvidersInternal<T>(item, libraryOptions, options, true, true).ToList();
// Locals
- list.AddRange(providers.Where(i => (i is ILocalMetadataProvider)).Select(i => new MetadataPlugin
+ list.AddRange(providers.Where(i => i is ILocalMetadataProvider).Select(i => new MetadataPlugin
{
Name = i.Name,
Type = MetadataPluginType.LocalMetadataProvider
}));
// Fetchers
- list.AddRange(providers.Where(i => (i is IRemoteMetadataProvider)).Select(i => new MetadataPlugin
+ list.AddRange(providers.Where(i => i is IRemoteMetadataProvider).Select(i => new MetadataPlugin
{
Name = i.Name,
Type = MetadataPluginType.MetadataFetcher
@@ -569,12 +615,10 @@ namespace MediaBrowser.Providers.Manager
}));
}
- private void AddImagePlugins<T>(List<MetadataPlugin> list, T item, List<IImageProvider> imageProviders)
- where T : BaseItem
+ private void AddImagePlugins(List<MetadataPlugin> list, List<IImageProvider> imageProviders)
{
-
// Locals
- list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin
+ list.AddRange(imageProviders.Where(i => i is ILocalImageProvider).Select(i => new MetadataPlugin
{
Name = i.Name,
Type = MetadataPluginType.LocalImageProvider
@@ -588,6 +632,7 @@ namespace MediaBrowser.Providers.Manager
}));
}
+ /// <inheritdoc/>
public MetadataOptions GetMetadataOptions(BaseItem item)
{
var type = item.GetType().Name;
@@ -597,17 +642,13 @@ namespace MediaBrowser.Providers.Manager
new MetadataOptions();
}
- /// <summary>
- /// Saves the metadata.
- /// </summary>
+ /// <inheritdoc/>
public void SaveMetadata(BaseItem item, ItemUpdateType updateType)
{
SaveMetadata(item, updateType, _savers);
}
- /// <summary>
- /// Saves the metadata.
- /// </summary>
+ /// <inheritdoc/>
public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
{
SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)));
@@ -619,7 +660,6 @@ namespace MediaBrowser.Providers.Manager
/// <param name="item">The item.</param>
/// <param name="updateType">Type of the update.</param>
/// <param name="savers">The savers.</param>
- /// <returns>Task.</returns>
private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
{
var libraryOptions = _libraryManager.GetLibraryOptions(item);
@@ -628,11 +668,9 @@ namespace MediaBrowser.Providers.Manager
{
_logger.LogDebug("Saving {0} to {1}.", item.Path ?? item.Name, saver.Name);
- var fileSaver = saver as IMetadataFileSaver;
-
- if (fileSaver != null)
+ if (saver is IMetadataFileSaver fileSaver)
{
- string path = null;
+ string path;
try
{
@@ -699,11 +737,9 @@ namespace MediaBrowser.Providers.Manager
{
if (updateType >= ItemUpdateType.MetadataEdit)
{
- var fileSaver = saver as IMetadataFileSaver;
-
// Manual edit occurred
// Even if save local is off, save locally anyway if the metadata file already exists
- if (fileSaver == null || !File.Exists(fileSaver.GetSavePath(item)))
+ if (!(saver is IMetadataFileSaver fileSaver) || !File.Exists(fileSaver.GetSavePath(item)))
{
return false;
}
@@ -734,6 +770,7 @@ namespace MediaBrowser.Providers.Manager
}
}
+ /// <inheritdoc/>
public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken)
where TItemType : BaseItem, new()
where TLookupType : ItemLookupInfo
@@ -748,7 +785,7 @@ namespace MediaBrowser.Providers.Manager
return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken);
}
- public async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken)
+ private async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken)
where TItemType : BaseItem, new()
where TLookupType : ItemLookupInfo
{
@@ -837,7 +874,9 @@ namespace MediaBrowser.Providers.Manager
return resultList;
}
- private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(IRemoteSearchProvider<TLookupType> provider, TLookupType searchInfo,
+ private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(
+ IRemoteSearchProvider<TLookupType> provider,
+ TLookupType searchInfo,
CancellationToken cancellationToken)
where TLookupType : ItemLookupInfo
{
@@ -853,7 +892,8 @@ namespace MediaBrowser.Providers.Manager
return list;
}
- public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
+ /// <inheritdoc/>
+ public Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
{
var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
@@ -865,6 +905,7 @@ namespace MediaBrowser.Providers.Manager
return provider.GetImageResponse(url, cancellationToken);
}
+ /// <inheritdoc/>
public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
{
return _externalIds.Where(i =>
@@ -881,6 +922,7 @@ namespace MediaBrowser.Providers.Manager
});
}
+ /// <inheritdoc/>
public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
{
return GetExternalIds(item)
@@ -909,6 +951,7 @@ namespace MediaBrowser.Providers.Manager
}).Where(i => i != null).Concat(item.GetRelatedUrls());
}
+ /// <inheritdoc/>
public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
{
return GetExternalIds(item)
@@ -921,8 +964,7 @@ namespace MediaBrowser.Providers.Manager
});
}
- private ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>();
-
+ /// <inheritdoc/>
public Dictionary<Guid, Guid> GetRefreshQueue()
{
lock (_refreshQueueLock)
@@ -938,22 +980,25 @@ namespace MediaBrowser.Providers.Manager
}
}
+ /// <inheritdoc/>
public void OnRefreshStart(BaseItem item)
{
- _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
+ _logger.LogDebug("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
_activeRefreshes[item.Id] = 0;
RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
}
+ /// <inheritdoc/>
public void OnRefreshComplete(BaseItem item)
{
- _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
+ _logger.LogDebug("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
_activeRefreshes.Remove(item.Id, out _);
RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
}
+ /// <inheritdoc/>
public double? GetRefreshProgress(Guid id)
{
if (_activeRefreshes.TryGetValue(id, out double value))
@@ -964,6 +1009,7 @@ namespace MediaBrowser.Providers.Manager
return null;
}
+ /// <inheritdoc/>
public void OnRefreshProgress(BaseItem item, double progress)
{
var id = item.Id;
@@ -983,12 +1029,7 @@ namespace MediaBrowser.Providers.Manager
RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
}
- private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
- new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>();
-
- private readonly object _refreshQueueLock = new object();
- private bool _isProcessingRefreshQueue;
-
+ /// <inheritdoc/>
public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority)
{
if (_disposed)
@@ -1032,7 +1073,7 @@ namespace MediaBrowser.Providers.Manager
if (item != null)
{
// Try to throttle this a little bit.
- await Task.Delay(100).ConfigureAwait(false);
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
var task = item is MusicArtist artist
? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
@@ -1062,17 +1103,14 @@ namespace MediaBrowser.Providers.Manager
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
// Collection folders don't validate their children so we'll have to simulate that here
-
- if (item is CollectionFolder collectionFolder)
+ switch (item)
{
- await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- if (item is Folder folder)
- {
+ case CollectionFolder collectionFolder:
+ await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
+ break;
+ case Folder folder:
await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
- }
+ break;
}
}
@@ -1082,7 +1120,7 @@ namespace MediaBrowser.Providers.Manager
{
await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
- await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true).ConfigureAwait(false);
+ await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
}
}
@@ -1118,20 +1156,41 @@ namespace MediaBrowser.Providers.Manager
}
}
+ /// <inheritdoc/>
public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return RefreshItem(item, options, cancellationToken);
}
- private bool _disposed;
+ /// <inheritdoc/>
public void Dispose()
{
- _disposed = true;
+ 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 (!_disposeCancellationTokenSource.IsCancellationRequested)
{
_disposeCancellationTokenSource.Cancel();
}
+
+ if (disposing)
+ {
+ _disposeCancellationTokenSource.Dispose();
+ }
+
+ _disposed = true;
}
}
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 446e27df6..85966b3bf 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,18 +16,19 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
- <PackageReference Include="PlaylistsNET" Version="1.0.6" />
- <PackageReference Include="TvDbSharper" Version="3.2.0" />
+ <PackageReference Include="PlaylistsNET" Version="1.1.2" />
+ <PackageReference Include="TvDbSharper" Version="3.2.1" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+ <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index 80acb2c05..f69ec9744 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.MediaInfo
private string GetAudioImagePath(Audio item)
{
- string filename = null;
+ string filename;
if (item.GetType() == typeof(Audio))
{
@@ -116,9 +116,9 @@ namespace MediaBrowser.Providers.MediaInfo
filename = item.Id.ToString("N", CultureInfo.InvariantCulture) + ".jpg";
}
- var prefix = filename.Substring(0, 1);
+ var prefix = filename.AsSpan().Slice(0, 1);
- return Path.Combine(AudioImagesPath, prefix, filename);
+ return Path.Join(AudioImagesPath, prefix, filename);
}
public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
index 69c6fd722..77f03580a 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
@@ -2,11 +2,9 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -17,7 +15,6 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.MediaInfo
{
@@ -25,19 +22,17 @@ namespace MediaBrowser.Providers.MediaInfo
{
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
- private readonly IApplicationPaths _appPaths;
- private readonly IJsonSerializer _json;
private readonly ILibraryManager _libraryManager;
private readonly IMediaSourceManager _mediaSourceManager;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- public FFProbeAudioInfo(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IApplicationPaths appPaths, IJsonSerializer json, ILibraryManager libraryManager)
+ public FFProbeAudioInfo(
+ IMediaSourceManager mediaSourceManager,
+ IMediaEncoder mediaEncoder,
+ IItemRepository itemRepo,
+ ILibraryManager libraryManager)
{
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
- _appPaths = appPaths;
- _json = json;
_libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager;
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index 4fabe709b..9926275ae 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -40,19 +40,15 @@ namespace MediaBrowser.Providers.MediaInfo
IHasItemChangeMonitor
{
private readonly ILogger<FFProbeProvider> _logger;
- private readonly IIsoManager _isoManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
private readonly IBlurayExaminer _blurayExaminer;
private readonly ILocalizationManager _localization;
- private readonly IApplicationPaths _appPaths;
- private readonly IJsonSerializer _json;
private readonly IEncodingManager _encodingManager;
private readonly IServerConfigurationManager _config;
private readonly ISubtitleManager _subtitleManager;
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
- private readonly IChannelManager _channelManager;
private readonly IMediaSourceManager _mediaSourceManager;
public string Name => "ffprobe";
@@ -126,14 +122,10 @@ namespace MediaBrowser.Providers.MediaInfo
public FFProbeProvider(
ILogger<FFProbeProvider> logger,
IMediaSourceManager mediaSourceManager,
- IChannelManager channelManager,
- IIsoManager isoManager,
IMediaEncoder mediaEncoder,
IItemRepository itemRepo,
IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
- IApplicationPaths appPaths,
- IJsonSerializer json,
IEncodingManager encodingManager,
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
@@ -141,19 +133,15 @@ namespace MediaBrowser.Providers.MediaInfo
ILibraryManager libraryManager)
{
_logger = logger;
- _isoManager = isoManager;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
_blurayExaminer = blurayExaminer;
_localization = localization;
- _appPaths = appPaths;
- _json = json;
_encodingManager = encodingManager;
_config = config;
_subtitleManager = subtitleManager;
_chapterManager = chapterManager;
_libraryManager = libraryManager;
- _channelManager = channelManager;
_mediaSourceManager = mediaSourceManager;
_subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
@@ -211,9 +199,9 @@ namespace MediaBrowser.Providers.MediaInfo
private string NormalizeStrmLine(string line)
{
- return line.Replace("\t", string.Empty)
- .Replace("\r", string.Empty)
- .Replace("\n", string.Empty)
+ return line.Replace("\t", string.Empty, StringComparison.Ordinal)
+ .Replace("\r", string.Empty, StringComparison.Ordinal)
+ .Replace("\n", string.Empty, StringComparison.Ordinal)
.Trim();
}
@@ -242,10 +230,11 @@ namespace MediaBrowser.Providers.MediaInfo
FetchShortcutInfo(item);
}
- var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _appPaths, _json, _libraryManager);
+ var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _libraryManager);
return prober.Probe(item, options, cancellationToken);
}
+
// Run last
public int Order => 100;
}
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index 64a5e7c8e..43659b68c 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
namespace MediaBrowser.Providers.MediaInfo
{
diff --git a/MediaBrowser.Providers/Music/Extensions.cs b/MediaBrowser.Providers/Music/Extensions.cs
index b57d35256..dddfd02e4 100644
--- a/MediaBrowser.Providers/Music/Extensions.cs
+++ b/MediaBrowser.Providers/Music/Extensions.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Entities;
namespace MediaBrowser.Providers.Music
{
- public static class Extensions
+ public static class AlbumInfoExtensions
{
public static string GetAlbumArtist(this AlbumInfo info)
{
@@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.Music
return id;
}
- return info.AlbumArtists.FirstOrDefault();
+ return info.AlbumArtists.Count > 0 ? info.AlbumArtists[0] : default;
}
public static string GetReleaseGroupId(this AlbumInfo info)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
index b211ed8b7..c9dac9ecd 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
@@ -1,9 +1,9 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -17,13 +17,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public class AudioDbAlbumImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IServerConfigurationManager _config;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _json;
- public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IJsonSerializer json)
+ public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IJsonSerializer json)
{
_config = config;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_json = json;
}
@@ -94,13 +94,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
}
/// <inheritdoc />
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ var httpClient = _httpClientFactory.CreateClient();
+ return httpClient.GetAsync(url, cancellationToken);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
index 7e54fcbdd..321144edf 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
@@ -10,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
@@ -26,16 +25,16 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _json;
public static AudioDbAlbumProvider Current;
- public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json)
+ public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
{
_config = config;
_fileSystem = fileSystem;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_json = json;
Current = this;
@@ -174,18 +173,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
- using (var httpResponse = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (var response = httpResponse.Content)
- using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
- {
- await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
- }
+ using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
private static string GetAlbumDataPath(IApplicationPaths appPaths, string musicBrainzReleaseGroupId)
@@ -294,7 +285,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
}
/// <inheritdoc />
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
index 243b62f7b..651266868 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
@@ -1,9 +1,9 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -17,14 +17,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public class AudioDbArtistImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IServerConfigurationManager _config;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _json;
- public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClient httpClient)
+ public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClientFactory httpClientFactory)
{
_config = config;
_json = json;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
}
/// <inheritdoc />
@@ -135,13 +135,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
return list;
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ var httpClient = _httpClientFactory.CreateClient();
+ return httpClient.GetAsync(url, cancellationToken);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
index 892f73422..708426500 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
@@ -9,7 +9,6 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
@@ -25,7 +24,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _json;
public static AudioDbArtistProvider Current;
@@ -33,11 +32,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private const string ApiKey = "195003";
public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey;
- public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json)
+ public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
{
_config = config;
_fileSystem = fileSystem;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_json = json;
Current = this;
}
@@ -155,23 +154,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
- using (var httpResponse = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- BufferContent = true
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (var response = httpResponse.Content)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
- {
- await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
- }
- }
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -289,7 +278,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
}
/// <inheritdoc />
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
index fbf413f2b..82f26a8f2 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
@@ -28,29 +28,31 @@
pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8"
};
- $('.configPage').on('pageshow', function () {
- Dashboard.showLoadingMsg();
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- $('#enable').checked = config.Enable;
- $('#replaceAlbumName').checked = config.ReplaceAlbumName;
-
- Dashboard.hideLoadingMsg();
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ document.querySelector('#enable').checked = config.Enable;
+ document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName;
+
+ Dashboard.hideLoadingMsg();
+ });
});
- });
-
- $('.configForm').on('submit', function (e) {
- Dashboard.showLoadingMsg();
- var form = this;
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- config.Enable = $('#enable', form).checked;
- config.ReplaceAlbumName = $('#replaceAlbumName', form).checked;
-
- ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.Enable = document.querySelector('#enable').checked;
+ config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked;
+
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
});
-
- return false;
- });
</script>
</div>
</body>
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
index cb7c0362e..54054d015 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
@@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public override string Description => "Get artist and album metadata or images from AudioDB.";
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs
index 9f36a03f9..3550614dd 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs
@@ -8,12 +8,12 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Music
internal static MusicBrainzAlbumProvider Current;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IApplicationHost _appHost;
private readonly ILogger<MusicBrainzAlbumProvider> _logger;
@@ -51,11 +51,11 @@ namespace MediaBrowser.Providers.Music
private Stopwatch _stopWatchMusicBrainz = new Stopwatch();
public MusicBrainzAlbumProvider(
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IApplicationHost appHost,
ILogger<MusicBrainzAlbumProvider> logger)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
_logger = logger;
@@ -123,11 +123,9 @@ namespace MediaBrowser.Providers.Music
if (!string.IsNullOrWhiteSpace(url))
{
- using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- using (var stream = response.Content)
- {
- return GetResultsFromResponse(stream);
- }
+ using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return GetResultsFromResponse(stream);
}
return Enumerable.Empty<RemoteSearchResult>();
@@ -278,27 +276,23 @@ namespace MediaBrowser.Providers.Music
private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken)
{
- var url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}",
+ var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/release/?query=\"{0}\" AND arid:{1}",
WebUtility.UrlEncode(albumName),
artistId);
- using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- using (var stream = response.Content)
- using (var oReader = new StreamReader(stream, Encoding.UTF8))
+ using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ using var oReader = new StreamReader(stream, Encoding.UTF8);
+ var settings = new XmlReaderSettings
{
- var settings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
+ ValidationType = ValidationType.None,
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true
+ };
- using (var reader = XmlReader.Create(oReader, settings))
- {
- return ReleaseResult.Parse(reader).FirstOrDefault();
- }
- }
+ using var reader = XmlReader.Create(oReader, settings);
+ return ReleaseResult.Parse(reader).FirstOrDefault();
}
private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
@@ -309,23 +303,19 @@ namespace MediaBrowser.Providers.Music
WebUtility.UrlEncode(albumName),
WebUtility.UrlEncode(artistName));
- using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- using (var stream = response.Content)
- using (var oReader = new StreamReader(stream, Encoding.UTF8))
+ using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ using var oReader = new StreamReader(stream, Encoding.UTF8);
+ var settings = new XmlReaderSettings()
{
- var settings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
+ ValidationType = ValidationType.None,
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true
+ };
- using (var reader = XmlReader.Create(oReader, settings))
- {
- return ReleaseResult.Parse(reader).FirstOrDefault();
- }
- }
+ using var reader = XmlReader.Create(oReader, settings);
+ return ReleaseResult.Parse(reader).FirstOrDefault();
}
private class ReleaseResult
@@ -624,30 +614,21 @@ namespace MediaBrowser.Providers.Music
{
var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture);
- using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- using (var stream = response.Content)
- using (var oReader = new StreamReader(stream, Encoding.UTF8))
+ using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ using var oReader = new StreamReader(stream, Encoding.UTF8);
+ var settings = new XmlReaderSettings
{
- var settings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
-
- using (var reader = XmlReader.Create(oReader, settings))
- {
- var result = ReleaseResult.Parse(reader).FirstOrDefault();
+ ValidationType = ValidationType.None,
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true
+ };
- if (result != null)
- {
- return result.ReleaseId;
- }
- }
- }
+ using var reader = XmlReader.Create(oReader, settings);
+ var result = ReleaseResult.Parse(reader).FirstOrDefault();
- return null;
+ return result?.ReleaseId;
}
/// <summary>
@@ -660,59 +641,57 @@ namespace MediaBrowser.Providers.Music
{
var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture);
- using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- using (var stream = response.Content)
- using (var oReader = new StreamReader(stream, Encoding.UTF8))
+ using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ using var oReader = new StreamReader(stream, Encoding.UTF8);
+ var settings = new XmlReaderSettings
{
- var settings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
+ ValidationType = ValidationType.None,
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true
+ };
- using (var reader = XmlReader.Create(oReader, settings))
- {
- reader.MoveToContent();
- reader.Read();
+ using (var reader = XmlReader.Create(oReader, settings))
+ {
+ reader.MoveToContent();
+ reader.Read();
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
{
- if (reader.NodeType == XmlNodeType.Element)
+ switch (reader.Name)
{
- switch (reader.Name)
+ case "release-group-list":
{
- case "release-group-list":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
- using (var subReader = reader.ReadSubtree())
- {
- return GetFirstReleaseGroupId(subReader);
- }
- }
+ using (var subReader = reader.ReadSubtree())
+ {
+ return GetFirstReleaseGroupId(subReader);
+ }
+ }
- default:
- {
- reader.Skip();
- break;
- }
+ default:
+ {
+ reader.Skip();
+ break;
}
}
- else
- {
- reader.Read();
- }
}
-
- return null;
+ else
+ {
+ reader.Read();
+ }
}
+
+ return null;
}
}
@@ -755,23 +734,19 @@ namespace MediaBrowser.Providers.Music
/// A number of retries shall be made in order to try and satisfy the request before
/// giving up and returning null.
/// </summary>
- internal async Task<HttpResponseInfo> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
+ internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
{
- var options = new HttpRequestOptions
- {
- Url = _musicBrainzBaseUrl.TrimEnd('/') + url,
- CancellationToken = cancellationToken,
- // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
- // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
- UserAgent = string.Format(
- CultureInfo.InvariantCulture,
- "{0} ( {1} )",
- _appHost.ApplicationUserAgent,
- _appHost.ApplicationUserAgentAddress),
- BufferContent = false
- };
+ using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url);
+
+ // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
+ // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
+ options.Headers.UserAgent.Add(new ProductInfoHeaderValue(string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} ( {1} )",
+ _appHost.ApplicationUserAgent,
+ _appHost.ApplicationUserAgentAddress)));
- HttpResponseInfo response;
+ HttpResponseMessage response;
var attempts = 0u;
do
@@ -790,7 +765,7 @@ namespace MediaBrowser.Providers.Music
_logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
_stopWatchMusicBrainz.Restart();
- response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+ response = await _httpClientFactory.CreateClient().SendAsync(options).ConfigureAwait(false);
// We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
}
@@ -799,14 +774,14 @@ namespace MediaBrowser.Providers.Music
// Log error if unable to query MB database due to throttling
if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
{
- _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.Url);
+ _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri);
}
return response;
}
/// <inheritdoc />
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
index 955766403..781b71640 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
@@ -6,11 +6,11 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
@@ -37,21 +37,19 @@ namespace MediaBrowser.Providers.Music
{
var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture);
- using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- using (var stream = response.Content)
- {
- return GetResultsFromResponse(stream);
- }
+ using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return GetResultsFromResponse(stream);
}
else
{
// They seem to throw bad request failures on any term with a slash
var nameToSearch = searchInfo.Name.Replace('/', ' ');
- var url = string.Format("/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch));
+ var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch));
using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- using (var stream = response.Content)
+ await using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
var results = GetResultsFromResponse(stream).ToList();
@@ -64,15 +62,11 @@ namespace MediaBrowser.Providers.Music
if (HasDiacritics(searchInfo.Name))
{
// Try again using the search with accent characters url
- url = string.Format("/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
+ url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
- using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- {
- using (var stream = response.Content)
- {
- return GetResultsFromResponse(stream);
- }
- }
+ using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return GetResultsFromResponse(stream);
}
}
@@ -298,7 +292,7 @@ namespace MediaBrowser.Providers.Music
public string Name => "MusicBrainz";
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
index 90196b046..1945e6cb4 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
@@ -36,33 +36,47 @@
uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
};
- $('.musicBrainzConfigPage').on('pageshow', function () {
- Dashboard.showLoadingMsg();
- ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
- $('#server').val(config.Server).change();
- $('#rateLimit').val(config.RateLimit).change();
- $('#enable').checked = config.Enable;
- $('#replaceArtistName').checked = config.ReplaceArtistName;
+ document.querySelector('.musicBrainzConfigPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+ var server = document.querySelector('#server');
+ server.value = config.Server;
+ server.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ var rateLimit = document.querySelector('#rateLimit');
+ rateLimit.value = config.RateLimit;
+ rateLimit.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ document.querySelector('#enable').checked = config.Enable;
+ document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName;
- Dashboard.hideLoadingMsg();
+ Dashboard.hideLoadingMsg();
+ });
});
- });
-
- $('.musicBrainzConfigForm').on('submit', function (e) {
- Dashboard.showLoadingMsg();
-
- var form = this;
- ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
- config.Server = $('#server', form).val();
- config.RateLimit = $('#rateLimit', form).val();
- config.Enable = $('#enable', form).checked;
- config.ReplaceArtistName = $('#replaceArtistName', form).checked;
-
- ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+
+ document.querySelector('.musicBrainzConfigForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+ config.Server = document.querySelector('#server').value;
+ config.RateLimit = document.querySelector('#rateLimit').value;
+ config.Enable = document.querySelector('#enable').checked;
+ config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked;
+
+ ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
});
-
- return false;
- });
</script>
</div>
</body>
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
index a7e6267da..90266e440 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
@@ -23,6 +23,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
public const long DefaultRateLimit = 2000u;
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
index 8b117ec8d..f4375b3cb 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
@@ -24,25 +24,28 @@
pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8"
};
- $('.configPage').on('pageshow', function () {
- Dashboard.showLoadingMsg();
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- $('#castAndCrew').checked = config.CastAndCrew;
- Dashboard.hideLoadingMsg();
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ document.querySelector('#castAndCrew').checked = config.CastAndCrew;
+ Dashboard.hideLoadingMsg();
+ });
});
- });
- $('.configForm').on('submit', function (e) {
- Dashboard.showLoadingMsg();
-
- var form = this;
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- config.CastAndCrew = $('#castAndCrew', form).checked;
- ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+
+ 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;
});
-
- return false;
- });
</script>
</div>
</body>
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index 50d6b78ae..bfc840ea5 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
@@ -1,10 +1,10 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly OmdbItemProvider _itemProvider;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
@@ -28,17 +28,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public OmdbEpisodeProvider(
IJsonSerializer jsonSerializer,
IApplicationHost appHost,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
- _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager);
+ _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
}
// After TheTvDb
@@ -69,7 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue)
{
- result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager)
+ result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager)
.FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
}
@@ -77,7 +77,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return result;
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _itemProvider.GetImageResponse(url, cancellationToken);
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
index 2d09a66c3..c18725e0a 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -1,10 +1,11 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Net.Http;
+using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -19,16 +20,16 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IApplicationHost _appHost;
- public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+ public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
@@ -48,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var list = new List<RemoteImageInfo>();
- var provider = new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager);
+ var provider = new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager);
if (!string.IsNullOrWhiteSpace(imdbId))
{
@@ -70,7 +71,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
list.Add(new RemoteImageInfo
{
ProviderName = Name,
- Url = string.Format("https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId)
+ Url = string.Format(CultureInfo.InvariantCulture, "https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId)
});
}
}
@@ -79,13 +80,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return list;
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
public string Name => "The Open Movie Database";
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 944ba26af..102ad82e1 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -5,10 +5,10 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder
{
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
@@ -35,13 +35,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public OmdbItemProvider(
IJsonSerializer jsonSerializer,
IApplicationHost appHost,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
@@ -127,69 +127,65 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- var url = OmdbProvider.GetOmdbUrl(urlQuery, _appHost, cancellationToken);
+ var url = OmdbProvider.GetOmdbUrl(urlQuery);
- using (var response = await OmdbProvider.GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
+ using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var resultList = new List<SearchResult>();
+
+ if (isSearch)
{
- using (var stream = response.Content)
+ var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false);
+ if (searchResultList != null && searchResultList.Search != null)
{
- var resultList = new List<SearchResult>();
-
- if (isSearch)
- {
- var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false);
- if (searchResultList != null && searchResultList.Search != null)
- {
- resultList.AddRange(searchResultList.Search);
- }
- }
- else
- {
- var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false);
- if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
- {
- resultList.Add(result);
- }
- }
-
- return resultList.Select(result =>
- {
- var item = new RemoteSearchResult
- {
- IndexNumber = searchInfo.IndexNumber,
- Name = result.Title,
- ParentIndexNumber = searchInfo.ParentIndexNumber,
- SearchProviderName = Name
- };
-
- if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue)
- {
- item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value;
- }
-
- item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
-
- if (result.Year.Length > 0
- && int.TryParse(result.Year.Substring(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
- {
- item.ProductionYear = parsedYear;
- }
-
- if (!string.IsNullOrEmpty(result.Released)
- && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
- {
- item.PremiereDate = released;
- }
-
- if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase))
- {
- item.ImageUrl = result.Poster;
- }
-
- return item;
- });
+ resultList.AddRange(searchResultList.Search);
}
}
+ else
+ {
+ var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false);
+ if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
+ {
+ resultList.Add(result);
+ }
+ }
+
+ return resultList.Select(result =>
+ {
+ var item = new RemoteSearchResult
+ {
+ IndexNumber = searchInfo.IndexNumber,
+ Name = result.Title,
+ ParentIndexNumber = searchInfo.ParentIndexNumber,
+ SearchProviderName = Name
+ };
+
+ if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue)
+ {
+ item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value;
+ }
+
+ item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
+
+ if (result.Year.Length > 0
+ && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
+ {
+ item.ProductionYear = parsedYear;
+ }
+
+ if (!string.IsNullOrEmpty(result.Released)
+ && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
+ {
+ item.PremiereDate = released;
+ }
+
+ if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ImageUrl = result.Poster;
+ }
+
+ return item;
+ });
}
public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
@@ -224,7 +220,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
@@ -256,7 +252,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
@@ -276,13 +272,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return first == null ? null : first.GetProviderId(MetadataProvider.Imdb);
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
class SearchResult
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 9700f3b18..c45149c3a 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -10,7 +10,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
@@ -25,14 +24,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IApplicationHost _appHost;
- public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
+ public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
@@ -62,7 +61,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
+ && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;
@@ -163,7 +162,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
+ && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;
@@ -257,16 +256,16 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return false;
}
- public static string GetOmdbUrl(string query, IApplicationHost appHost, CancellationToken cancellationToken)
+ public static string GetOmdbUrl(string query)
{
- const string url = "https://www.omdbapi.com?apikey=2c9d9507";
+ const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
if (string.IsNullOrWhiteSpace(query))
{
- return url;
+ return Url;
}
- return url + "&" + query;
+ return Url + "&" + query;
}
private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
@@ -291,17 +290,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- var url = GetOmdbUrl(string.Format("i={0}&plot=short&tomatoes=true&r=json", imdbParam), _appHost, cancellationToken);
+ var url = GetOmdbUrl(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "i={0}&plot=short&tomatoes=true&r=json",
+ imdbParam));
- using (var response = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
- {
- using (var stream = response.Content)
- {
- var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(rootObject, path);
- }
- }
+ using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ _jsonSerializer.SerializeToFile(rootObject, path);
return path;
}
@@ -328,30 +327,25 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- var url = GetOmdbUrl(string.Format("i={0}&season={1}&detail=full", imdbParam, seasonId), _appHost, cancellationToken);
+ var url = GetOmdbUrl(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "i={0}&season={1}&detail=full",
+ imdbParam,
+ seasonId));
- using (var response = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
- {
- using (var stream = response.Content)
- {
- var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(rootObject, path);
- }
- }
+ using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ _jsonSerializer.SerializeToFile(rootObject, path);
return path;
}
- public static Task<HttpResponseInfo> GetOmdbResponse(IHttpClient httpClient, string url, CancellationToken cancellationToken)
+ public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken)
{
- return httpClient.SendAsync(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- BufferContent = true,
- EnableDefaultUserAgent = true
- }, HttpMethod.Get);
+ return httpClient.GetAsync(url, cancellationToken);
}
internal string GetDataFilePath(string imdbId)
@@ -363,7 +357,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
- var filename = string.Format("{0}.json", imdbId);
+ var filename = string.Format(CultureInfo.InvariantCulture, "{0}.json", imdbId);
return Path.Combine(dataPath, filename);
}
@@ -377,7 +371,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
- var filename = string.Format("{0}_season_{1}.json", imdbId, seasonId);
+ var filename = string.Format(CultureInfo.InvariantCulture, "{0}_season_{1}.json", imdbId, seasonId);
return Path.Combine(dataPath, filename);
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
index 4cf5f4ce6..41ca56164 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
@@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public override string Description => "Get metadata for movies and other video content from OMDb.";
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
index aa5f819f0..e7079ed3c 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
@@ -17,6 +17,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
public override string Description => "Get metadata for movies and other video content from TheTVDB.";
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
index 2c6682f82..f22d484ab 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
@@ -2,8 +2,10 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Providers;
@@ -18,7 +20,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
private const string DefaultLanguage = "en";
- private readonly SemaphoreSlim _cacheWriteLock = new SemaphoreSlim(1, 1);
private readonly IMemoryCache _cache;
private readonly TvDbClient _tvDbClient;
private DateTime _tokenCreatedAt;
@@ -175,7 +176,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
string language,
CancellationToken cancellationToken)
{
- searchInfo.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(),
+ searchInfo.SeriesProviderIds.TryGetValue(nameof(MetadataProvider.Tvdb),
out var seriesTvdbId);
var episodeQuery = new EpisodeQuery();
@@ -202,10 +203,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
else if (searchInfo.PremiereDate.HasValue)
{
// tvdb expects yyyy-mm-dd format
- episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd");
+ episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
- return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken);
+ return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), episodeQuery, language, cancellationToken);
}
public async Task<string> GetEpisodeTvdbId(
@@ -217,7 +218,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var episodePage =
await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken)
.ConfigureAwait(false);
- return episodePage.Data.FirstOrDefault()?.Id.ToString();
+ return episodePage.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(
@@ -229,30 +230,56 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
}
- private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
+ public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
{
- if (_cache.TryGetValue(key, out T cachedValue))
+ var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+ var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+ if (imagesSummary.Data.Fanart > 0)
{
- return cachedValue;
+ yield return KeyType.Fanart;
}
- await _cacheWriteLock.WaitAsync().ConfigureAwait(false);
- try
+ if (imagesSummary.Data.Series > 0)
{
- if (_cache.TryGetValue(key, out cachedValue))
- {
- return cachedValue;
- }
+ yield return KeyType.Series;
+ }
- _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage;
- var result = await resultFactory.Invoke().ConfigureAwait(false);
- _cache.Set(key, result, TimeSpan.FromHours(1));
- return result;
+ if (imagesSummary.Data.Poster > 0)
+ {
+ yield return KeyType.Poster;
}
- finally
+ }
+
+ public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+ var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+ if (imagesSummary.Data.Season > 0)
{
- _cacheWriteLock.Release();
+ yield return KeyType.Season;
}
+
+ if (imagesSummary.Data.Fanart > 0)
+ {
+ yield return KeyType.Fanart;
+ }
+
+ // TODO seasonwide is not supported in TvDbSharper
+ }
+
+ private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
+ {
+ if (_cache.TryGetValue(key, out T cachedValue))
+ {
+ return cachedValue;
+ }
+
+ _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage;
+ var result = await resultFactory.Invoke().ConfigureAwait(false);
+ _cache.Set(key, result, TimeSpan.FromHours(1));
+ return result;
}
private static string GenerateKey(params object[] objects)
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
index 9b87e3617..4e7c0e5a6 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
@@ -2,9 +2,10 @@
using System;
using System.Collections.Generic;
+using System.Net.Http;
+using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -18,13 +19,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbEpisodeImageProvider : IRemoteImageProvider
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbEpisodeImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
- public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager)
+ public TvdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
@@ -76,7 +77,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var episodeResult =
await _tvdbClientManager
- .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), language, cancellationToken)
+ .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken)
.ConfigureAwait(false);
var image = GetImageInfo(episodeResult.Data);
@@ -103,8 +104,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return new RemoteImageInfo
{
- Width = Convert.ToInt32(episode.ThumbWidth),
- Height = Convert.ToInt32(episode.ThumbHeight),
+ Width = Convert.ToInt32(episode.ThumbWidth, CultureInfo.InvariantCulture),
+ Height = Convert.ToInt32(episode.ThumbHeight, CultureInfo.InvariantCulture),
ProviderName = Name,
Url = TvdbUtils.BannerUrl + episode.Filename,
Type = ImageType.Primary
@@ -113,13 +114,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
index ced287d54..90436c7c9 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
@@ -2,9 +2,9 @@
using System;
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -21,13 +21,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
/// </summary>
public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbEpisodeProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
- public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager)
+ public TvdbEpisodeProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
@@ -188,7 +188,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
for (var i = 0; i < episode.GuestStars.Length; ++i)
{
var currentActor = episode.GuestStars[i];
- var roleStartIndex = currentActor.IndexOf('(');
+ var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal);
if (roleStartIndex == -1)
{
@@ -207,7 +207,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
for (var j = i + 1; j < episode.GuestStars.Length; ++j)
{
var currentRole = episode.GuestStars[j];
- var roleEndIndex = currentRole.IndexOf(')');
+ var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal);
if (roleEndIndex == -1)
{
@@ -242,13 +242,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return result;
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
public int Order => 0;
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
index 9db21f012..388a4e3e7 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -20,15 +20,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbPersonImageProvider> _logger;
private readonly ILibraryManager _libraryManager;
private readonly TvdbClientManager _tvdbClientManager;
- public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager)
+ public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager)
{
_libraryManager = libraryManager;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
@@ -104,13 +104,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
}
/// <inheritdoc />
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
index 5af99a573..ff8c3455f 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -20,13 +20,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeasonImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
- public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager)
+ public TvdbSeasonImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
@@ -65,8 +65,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>();
- var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart };
- foreach (var keyType in keyTypes)
+ var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
+ await foreach (var keyType in keyTypes)
{
var imageQuery = new ImagesQuery
{
@@ -146,13 +146,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
index 7dd012825..d287828eb 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -20,13 +20,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeriesImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
- public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager)
+ public TvdbSeriesImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
@@ -59,9 +59,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>();
- var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart };
var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
- foreach (KeyType keyType in keyTypes)
+ var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
+ .ConfigureAwait(false);
+ await foreach (KeyType keyType in allowedKeyTypes)
{
var imageQuery = new ImagesQuery
{
@@ -143,13 +144,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
index b3641dc9f..c6dd8a5f3 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
@@ -3,11 +3,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -25,15 +25,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
internal static TvdbSeriesProvider Current { get; private set; }
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeriesProvider> _logger;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localizationManager;
private readonly TvdbClientManager _tvdbClientManager;
- public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager)
+ public TvdbSeriesProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_libraryManager = libraryManager;
_localizationManager = localizationManager;
@@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
Name = tvdbTitles.FirstOrDefault(),
ProductionYear = firstAired.Year,
- SearchProviderName = Name,
- ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner
+ SearchProviderName = Name
};
+ if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
+ {
+ // Results from their Search endpoints already include the /banners/ part in the url, because reasons...
+ remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
+ }
+
try
{
var seriesSesult =
@@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
Type = PersonType.Actor,
Name = (actor.Name ?? string.Empty).Trim(),
Role = actor.Role,
- ImageUrl = TvdbUtils.BannerUrl + actor.Image,
SortOrder = actor.SortOrder
};
+ if (!string.IsNullOrEmpty(actor.Image))
+ {
+ personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image;
+ }
+
if (!string.IsNullOrWhiteSpace(personInfo.Name))
{
result.AddPerson(personInfo);
@@ -399,14 +408,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url,
- BufferContent = false
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
index 3f71041b2..37a8d04a6 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
@@ -9,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
public const string TvdbBaseUrl = "https://www.thetvdb.com/";
- public const string BannerUrl = TvdbBaseUrl + "banners/";
+ public const string TvdbImageBaseUrl = "https://www.thetvdb.com";
+ public const string BannerUrl = TvdbImageBaseUrl + "/banners/";
public static ImageType GetImageTypeFromKeyType(string keyType)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index c41bd925e..f43444028 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
@@ -20,11 +20,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
- public TmdbBoxSetImageProvider(IHttpClient httpClient)
+ public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
}
public string Name => ProviderName;
@@ -153,13 +153,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index 20b6cd505..4da2c042f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -5,10 +5,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
@@ -36,7 +37,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
public TmdbBoxSetProvider(
@@ -45,7 +46,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
IServerConfigurationManager config,
IFileSystem fileSystem,
ILocalizationManager localization,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
{
_logger = logger;
@@ -53,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
_config = config;
_fileSystem = fileSystem;
_localization = localization;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
Current = this;
}
@@ -179,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
if (!string.IsNullOrEmpty(language))
{
- url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
+ url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
// Get images in english and with no language
url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
@@ -187,21 +188,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
cancellationToken.ThrowIfCancellationRequested();
- CollectionResult mainResult;
-
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false);
- }
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
+ using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false);
+
cancellationToken.ThrowIfCancellationRequested();
if (mainResult != null && string.IsNullOrEmpty(mainResult.Name))
@@ -216,18 +212,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
}
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false);
- }
+ langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
+
+ await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(langStream).ConfigureAwait(false);
}
}
@@ -258,7 +250,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
var path = GetDataPath(appPaths, tmdbId);
- var filename = string.Format("all-{0}.json", preferredLanguage ?? string.Empty);
+ var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage ?? string.Empty);
return Path.Combine(path, filename);
}
@@ -277,13 +269,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
return dataPath;
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
index 310c871ec..aac4420e8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
@@ -10,8 +10,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
public string Freebase_Mid { get; set; }
- public int Tvdb_Id { get; set; }
+ public int? Tvdb_Id { get; set; }
- public int Tvrage_Id { get; set; }
+ public int? Tvrage_Id { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
index 7566df8b6..704ebcd5a 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
@@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
public BelongsToCollection Belongs_To_Collection { get; set; }
- public int Budget { get; set; }
+ public long Budget { get; set; }
public List<Genre> Genres { get; set; }
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
public string Release_Date { get; set; }
- public int Revenue { get; set; }
+ public long Revenue { get; set; }
public int Runtime { get; set; }
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
index 27ca3759e..01a887eed 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
@@ -300,7 +300,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
movie.RemoteTrailers = movieData.Trailers.Youtube.Select(i => new MediaUrl
{
- Url = string.Format("https://www.youtube.com/watch?v={0}", i.Source),
+ Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", i.Source),
Name = i.Name
}).ToArray();
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
index 36a06fba7..60d7a599e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
@@ -4,9 +4,9 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
@@ -23,13 +23,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public class TmdbImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IFileSystem _fileSystem;
- public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem)
+ public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
}
@@ -202,13 +202,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 5e2b83294..51c0b7e83 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -6,11 +6,11 @@ using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -18,7 +18,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
@@ -31,10 +30,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// </summary>
public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder
{
+ private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
+ private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
+
internal static TmdbMovieProvider Current { get; private set; }
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<TmdbMovieProvider> _logger;
@@ -45,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public TmdbMovieProvider(
IJsonSerializer jsonSerializer,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager,
ILogger<TmdbMovieProvider> logger,
@@ -53,7 +55,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
IApplicationHost appHost)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_logger = logger;
@@ -146,24 +148,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return _tmdbSettings;
}
- using (HttpResponseInfo response = await GetMovieDbResponse(new HttpRequestOptions
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey));
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- Url = string.Format(TmdbConfigUrl, TmdbUtils.ApiKey),
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
- {
- using (Stream json = response.Content)
- {
- _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(json).ConfigureAwait(false);
-
- return _tmdbSettings;
- }
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- }
- private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
- private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
+ using var response = await GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(stream).ConfigureAwait(false);
+ return _tmdbSettings;
+ }
/// <summary>
/// Gets the movie data path.
@@ -245,7 +240,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
preferredLanguage = "alllang";
}
- var filename = string.Format("all-{0}.json", preferredLanguage);
+ var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage);
return Path.Combine(path, filename);
}
@@ -276,7 +271,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
languages.Add("en");
}
- return string.Join(",", languages.ToArray());
+ return string.Join(",", languages);
}
public static string NormalizeLanguage(string language)
@@ -321,52 +316,33 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// <returns>Task{CompleteMovieData}.</returns>
internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken)
{
- var url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey);
+ var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey);
if (!string.IsNullOrEmpty(language))
{
- url += string.Format("&language={0}", NormalizeLanguage(language));
+ url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language));
// Get images in english and with no language
url += "&include_image_language=" + GetImageLanguagesParam(language);
}
- MovieResult mainResult;
-
cancellationToken.ThrowIfCancellationRequested();
- // Cache if not using a tmdbId because we won't have the tmdb cache directory structure. So use the lower level cache.
- var cacheMode = isTmdbId ? CacheMode.None : CacheMode.Unconditional;
- var cacheLength = TimeSpan.FromDays(3);
-
- try
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var response = await GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader,
- CacheMode = cacheMode,
- CacheLength = cacheLength
- }).ConfigureAwait(false))
- {
- using (var json = response.Content)
- {
- mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false);
- }
- }
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- catch (HttpException ex)
- {
- // Return null so that callers know there is no metadata for this id
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
- {
- return null;
- }
- throw;
+ using var mainResponse = await GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ if (mainResponse.StatusCode == HttpStatusCode.NotFound)
+ {
+ return null;
}
+ await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
+
cancellationToken.ThrowIfCancellationRequested();
// If the language preference isn't english, then have the overview fallback to english if it's blank
@@ -377,7 +353,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
_logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English...");
- url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+ url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
if (!string.IsNullOrEmpty(language))
{
@@ -385,64 +361,38 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
url += "&include_image_language=" + GetImageLanguagesParam(language);
}
- using (var response = await GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader,
- CacheMode = cacheMode,
- CacheLength = cacheLength
- }).ConfigureAwait(false))
+ using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false);
-
- mainResult.Overview = englishResult.Overview;
- }
+ langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
+
+ using var langResponse = await GetMovieDbResponse(langRequestMessage).ConfigureAwait(false);
+
+ await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
+ mainResult.Overview = langResult.Overview;
}
return mainResult;
}
- private static long _lastRequestTicks;
- // The limit is 40 requests per 10 seconds
- private const int RequestIntervalMs = 300;
-
/// <summary>
/// Gets the movie db response.
/// </summary>
- internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options)
+ internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message)
{
- var delayTicks = (RequestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks);
- var delayMs = Math.Min(delayTicks / 10000, RequestIntervalMs);
-
- if (delayMs > 0)
- {
- _logger.LogDebug("Throttling Tmdb by {0} ms", delayMs);
- await Task.Delay(Convert.ToInt32(delayMs)).ConfigureAwait(false);
- }
-
- _lastRequestTicks = DateTime.UtcNow.Ticks;
-
- options.BufferContent = true;
- options.UserAgent = _appHost.ApplicationUserAgent;
-
- return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+ message.Headers.UserAgent.Add(new ProductInfoHeaderValue(_appHost.ApplicationUserAgent));
+ return _httpClientFactory.CreateClient().SendAsync(message);
}
/// <inheritdoc />
public int Order => 1;
/// <inheritdoc />
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
index 10935c655..2a6c6d035 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
@@ -5,10 +5,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -21,11 +22,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
public class TmdbSearch
{
+ private const string SearchUrl = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
+ private const string SearchUrlTvWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/tv?api_key={1}&query={0}&language={2}&first_air_date_year={3}";
+ private const string SearchUrlMovieWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/movie?api_key={1}&query={0}&language={2}&primary_release_year={3}";
+
private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled);
private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled);
- private static readonly Regex _cleanStopWords = new Regex(@"\b( # Start at word boundary
+ private static readonly Regex _cleanStopWords = new Regex(
+ @"\b( # Start at word boundary
19[0-9]{2}|20[0-9]{2}| # 1900-2099
S[0-9]{2}| # Season
E[0-9]{2}| # Episode
@@ -36,8 +42,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
).* # Match rest of string",
RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase);
- private const string _searchURL = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
-
private readonly ILogger _logger;
private readonly IJsonSerializer _json;
private readonly ILibraryManager _libraryManager;
@@ -123,7 +127,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
name2 = name2.Trim();
// Search again if the new name is different
- if (!string.Equals(name2, name) && !string.IsNullOrWhiteSpace(name2))
+ if (!string.Equals(name2, name, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(name2))
{
_logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year);
results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
@@ -163,103 +167,131 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
if (string.IsNullOrWhiteSpace(name))
{
- throw new ArgumentException("name");
+ throw new ArgumentException("String can't be null or empty.", nameof(name));
}
- var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type);
-
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ string url3;
+ if (year != null && string.Equals(type, "movie", StringComparison.OrdinalIgnoreCase))
+ {
+ url3 = string.Format(
+ CultureInfo.InvariantCulture,
+ SearchUrlMovieWithYear,
+ WebUtility.UrlEncode(name),
+ TmdbUtils.ApiKey,
+ language,
+ year);
+ }
+ else
{
- Url = url3,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
+ url3 = string.Format(
+ CultureInfo.InvariantCulture,
+ SearchUrl,
+ WebUtility.UrlEncode(name),
+ TmdbUtils.ApiKey,
+ language,
+ type);
+ }
- }).ConfigureAwait(false))
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(json).ConfigureAwait(false);
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ }
+
+ using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(stream).ConfigureAwait(false);
- var results = searchResults.Results ?? new List<MovieResult>();
+ var results = searchResults.Results ?? new List<MovieResult>();
- return results
- .Select(i =>
+ return results
+ .Select(i =>
+ {
+ var remoteResult = new RemoteSearchResult {SearchProviderName = TmdbMovieProvider.Current.Name, Name = i.Title ?? i.Name ?? i.Original_Title, ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path};
+
+ if (!string.IsNullOrWhiteSpace(i.Release_Date))
+ {
+ // These dates are always in this exact format
+ if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
{
- var remoteResult = new RemoteSearchResult
- {
- SearchProviderName = TmdbMovieProvider.Current.Name,
- Name = i.Title ?? i.Name ?? i.Original_Title,
- ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(i.Release_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
- {
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
- return remoteResult;
- })
- .ToList();
- }
- }
+ remoteResult.PremiereDate = r.ToUniversalTime();
+ remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+ }
+ }
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+
+ return remoteResult;
+ })
+ .ToList();
}
private async Task<List<RemoteSearchResult>> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(name))
{
- throw new ArgumentException("name");
+ throw new ArgumentException("String can't be null or empty.", nameof(name));
}
- var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv");
-
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ string url3;
+ if (year == null)
{
- Url = url3,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ url3 = string.Format(
+ CultureInfo.InvariantCulture,
+ SearchUrl,
+ WebUtility.UrlEncode(name),
+ TmdbUtils.ApiKey,
+ language,
+ "tv");
+ }
+ else
{
- using (var json = response.Content)
- {
- var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(json).ConfigureAwait(false);
+ url3 = string.Format(
+ CultureInfo.InvariantCulture,
+ SearchUrlTvWithYear,
+ WebUtility.UrlEncode(name),
+ TmdbUtils.ApiKey,
+ language,
+ year);
+ }
- var results = searchResults.Results ?? new List<TvResult>();
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
+ foreach (var header in TmdbUtils.AcceptHeaders)
+ {
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ }
+
+ using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(stream).ConfigureAwait(false);
- return results
- .Select(i =>
+ var results = searchResults.Results ?? new List<TvResult>();
+
+ return results
+ .Select(i =>
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ SearchProviderName = TmdbMovieProvider.Current.Name,
+ Name = i.Name ?? i.Original_Name,
+ ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
+ };
+
+ if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
+ {
+ // These dates are always in this exact format
+ if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
{
- var remoteResult = new RemoteSearchResult
- {
- SearchProviderName = TmdbMovieProvider.Current.Name,
- Name = i.Name ?? i.Original_Name,
- ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
- {
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
- return remoteResult;
- })
- .ToList();
- }
- }
+ remoteResult.PremiereDate = r.ToUniversalTime();
+ remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+ }
+ }
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+
+ return remoteResult;
+ })
+ .ToList();
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
index d4264dd4e..73e49ba5b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
@@ -2,9 +2,9 @@
using System;
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Providers;
@@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
public string Name => TmdbMovieProvider.Current.Name;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index 2faa9f835..f31a7faea 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
@@ -22,13 +22,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
private readonly IServerConfigurationManager _config;
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
- public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClient httpClient)
+ public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
{
_config = config;
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
}
public string Name => ProviderName;
@@ -127,13 +127,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 76d3f8224..e9fb5c703 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -6,11 +6,12 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
@@ -36,20 +37,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TmdbPersonProvider> _logger;
public TmdbPersonProvider(
IFileSystem fileSystem,
IServerConfigurationManager configurationManager,
IJsonSerializer jsonSerializer,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ILogger<TmdbPersonProvider> logger)
{
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
Current = this;
}
@@ -96,22 +97,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey);
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ }
- }).ConfigureAwait(false))
- {
- using (var json = response.Content)
- {
- var result = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(json).ConfigureAwait(false) ??
- new TmdbSearchResult<PersonSearchResult>();
+ var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- return result.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
- }
- }
+ var result2 = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(stream).ConfigureAwait(false)
+ ?? new TmdbSearchResult<PersonSearchResult>();
+
+ return result2.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
}
private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl)
@@ -230,30 +228,23 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id);
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- using (var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
- {
- await json.CopyToAsync(fs).ConfigureAwait(false);
- }
- }
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
+
+ using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+ await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ await response.Content.CopyToAsync(fs).ConfigureAwait(false);
}
private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
{
- var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
+ var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1);
- return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
+ return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId);
}
internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
@@ -266,13 +257,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return Path.Combine(appPaths.CachePath, "tmdb-people");
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index 77e4b2c56..eebecdac6 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
@@ -26,8 +26,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
IRemoteImageProvider,
IHasOrder
{
- public TmdbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+ : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
{ }
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
@@ -115,7 +115,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return images.Stills ?? new List<Still>();
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return GetResponse(url, cancellationToken);
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 0c55b91e0..90e3cea93 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -5,9 +5,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -27,8 +27,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
IRemoteMetadataProvider<Episode, EpisodeInfo>,
IHasOrder
{
- public TmdbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+ : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
{ }
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
@@ -111,7 +111,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (response.External_Ids.Tvdb_Id > 0)
{
- item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture));
+ item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
}
item.PremiereDate = response.Air_Date;
@@ -131,7 +131,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase))
{
- var videoUrl = string.Format("http://www.youtube.com/watch?v={0}", video.Key);
+ var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key);
item.AddTrailerUrl(videoUrl);
}
}
@@ -201,7 +201,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return result;
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return GetResponse(url, cancellationToken);
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
index 846e6095b..5705885b4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
@@ -3,9 +3,10 @@
using System;
using System.Globalization;
using System.IO;
+using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
@@ -19,16 +20,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public abstract class TmdbEpisodeProviderBase
{
private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
private readonly ILogger<TmdbEpisodeProviderBase> _logger;
- protected TmdbEpisodeProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+ protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_configurationManager = configurationManager;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
@@ -91,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
- var filename = string.Format("season-{0}-episode-{1}-{2}.json",
+ var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-episode-{1}-{2}.json",
seasonNumber.ToString(CultureInfo.InvariantCulture),
episodeNumber.ToString(CultureInfo.InvariantCulture),
preferredLanguage);
@@ -115,7 +116,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(language))
{
- url += string.Format("&language={0}", language);
+ url += string.Format(CultureInfo.InvariantCulture, "&language={0}", language);
}
var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
@@ -124,27 +125,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
cancellationToken.ThrowIfCancellationRequested();
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
- {
- using (var json = response.Content)
- {
- return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(json).ConfigureAwait(false);
- }
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
+
+ using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(stream).ConfigureAwait(false);
}
- protected Task<HttpResponseInfo> GetResponse(string url, CancellationToken cancellationToken)
+ protected Task<HttpResponseMessage> GetResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index 56b6e4483..787514d05 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -4,9 +4,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -22,12 +22,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
- public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient)
+ public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
}
public int Order => 1;
@@ -36,13 +36,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public static string ProviderName => TmdbUtils.ProviderName;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 11f21333c..e59504cc6 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -5,9 +5,10 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -26,7 +27,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>
{
private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos";
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
@@ -35,9 +36,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
internal static TmdbSeasonProvider Current { get; private set; }
- public TmdbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
+ public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_localization = localization;
@@ -75,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (seasonInfo.External_Ids.Tvdb_Id > 0)
{
- result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture));
+ result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
}
var credits = seasonInfo.Credits;
@@ -121,13 +122,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
}
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,
@@ -183,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
- var filename = string.Format("season-{0}-{1}.json",
+ var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-{1}.json",
seasonNumber.ToString(CultureInfo.InvariantCulture),
preferredLanguage);
@@ -206,7 +203,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(language))
{
- url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
+ url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
}
var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
@@ -215,18 +212,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
cancellationToken.ThrowIfCancellationRequested();
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(json).ConfigureAwait(false);
- }
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
+
+ using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(stream).ConfigureAwait(false);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 95c451493..f11eeb15b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -23,13 +23,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IFileSystem _fileSystem;
- public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem)
+ public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
{
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
}
@@ -180,13 +180,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// After tvdb and fanart
public int Order => 2;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index f142fd29c..0eded3233 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -5,10 +5,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -35,7 +36,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<TmdbSeriesProvider> _logger;
private readonly ILocalizationManager _localization;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
@@ -48,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
IServerConfigurationManager configurationManager,
ILogger<TmdbSeriesProvider> logger,
ILocalizationManager localization,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
{
_jsonSerializer = jsonSerializer;
@@ -56,7 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
_configurationManager = configurationManager;
_logger = logger;
_localization = localization;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
Current = this;
}
@@ -92,7 +93,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (obj.External_Ids.Tvdb_Id > 0)
{
- remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.ToString(_usCulture));
+ remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture));
}
return new[] { remoteResult };
@@ -268,12 +269,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (ids.Tvrage_Id > 0)
{
- series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.ToString(_usCulture));
+ series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.Value.ToString(_usCulture));
}
if (ids.Tvdb_Id > 0)
{
- series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.ToString(_usCulture));
+ series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.ToString(_usCulture));
}
}
@@ -413,24 +414,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
cancellationToken.ThrowIfCancellationRequested();
- SeriesResult mainResult;
-
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false);
+ mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ }
- if (!string.IsNullOrEmpty(language))
- {
- mainResult.ResultLanguage = language;
- }
- }
+ using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage);
+ await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ mainResult.ResultLanguage = language;
}
cancellationToken.ThrowIfCancellationRequested();
@@ -451,21 +447,18 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
}
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- using (var json = response.Content)
- {
- var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false);
-
- mainResult.Overview = englishResult.Overview;
- mainResult.ResultLanguage = "en";
- }
+ mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
+
+ using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).ConfigureAwait(false);
+
+ mainResult.Overview = englishResult.Overview;
+ mainResult.ResultLanguage = "en";
}
return mainResult;
@@ -503,7 +496,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
- var filename = string.Format("series-{0}.json", preferredLanguage ?? string.Empty);
+ var filename = string.Format(CultureInfo.InvariantCulture, "series-{0}.json", preferredLanguage ?? string.Empty);
return Path.Combine(path, filename);
}
@@ -515,38 +508,38 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
TmdbUtils.ApiKey,
externalSource);
- using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+ foreach (var header in TmdbUtils.AcceptHeaders)
{
- Url = url,
- CancellationToken = cancellationToken,
- AcceptHeader = TmdbUtils.AcceptHeader
- }).ConfigureAwait(false))
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ }
+
+ using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+ var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).ConfigureAwait(false);
+
+ if (result != null && result.Tv_Results != null)
{
- using (var json = response.Content)
+ var tv = result.Tv_Results.FirstOrDefault();
+
+ if (tv != null)
{
- var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(json).ConfigureAwait(false);
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
- if (result != null && result.Tv_Results != null)
+ var remoteResult = new RemoteSearchResult
{
- var tv = result.Tv_Results.FirstOrDefault();
-
- if (tv != null)
- {
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var remoteResult = new RemoteSearchResult
- {
- Name = tv.Name,
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path) ? null : tmdbImageUrl + tv.Poster_Path
- };
+ Name = tv.Name,
+ SearchProviderName = Name,
+ ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path)
+ ? null
+ : tmdbImageUrl + tv.Poster_Path
+ };
- remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
- return remoteResult;
- }
- }
+ return remoteResult;
}
}
@@ -556,13 +549,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// After TheTVDB
public int Order => 1;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index 2f1e8b791..1415d6976 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -1,4 +1,5 @@
using System;
+using System.Net.Mime;
using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
@@ -32,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <summary>
/// Value of the Accept header for requests to the provider.
/// </summary>
- public const string AcceptHeader = "application/json,image/*";
+ public static readonly string[] AcceptHeaders = { MediaTypeNames.Application.Json, "image/*" };
/// <summary>
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
index 7e2b06257..10374bde9 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
@@ -1,9 +1,9 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Providers;
@@ -13,11 +13,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
{
public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo>
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
- public TmdbTrailerProvider(IHttpClient httpClient)
+ public TmdbTrailerProvider(IHttpClientFactory httpClientFactory)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
}
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
@@ -34,13 +34,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- });
+ return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 25f8beb40..321153c6b 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -2,12 +2,12 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
@@ -20,13 +20,13 @@ namespace MediaBrowser.Providers.Studios
public class StudiosImageProvider : IRemoteImageProvider
{
private readonly IServerConfigurationManager _config;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IFileSystem _fileSystem;
- public StudiosImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
+ public StudiosImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
{
_config = config;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
}
@@ -101,33 +101,29 @@ namespace MediaBrowser.Providers.Studios
private string GetUrl(string image, string filename)
{
- return string.Format("https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studios/{0}/{1}.jpg", image, filename);
+ return string.Format(CultureInfo.InvariantCulture, "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studios/{0}/{1}.jpg", image, filename);
}
private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken)
{
const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studiothumbs.txt";
- return EnsureList(url, file, _httpClient, _fileSystem, cancellationToken);
+ return EnsureList(url, file, _fileSystem, cancellationToken);
}
private Task<string> EnsurePosterList(string file, CancellationToken cancellationToken)
{
const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studioposters.txt";
- return EnsureList(url, file, _httpClient, _fileSystem, cancellationToken);
+ return EnsureList(url, file, _fileSystem, cancellationToken);
}
public int Order => 0;
- public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url,
- BufferContent = false
- });
+ var httpClient = _httpClientFactory.CreateClient();
+ return httpClient.GetAsync(url, cancellationToken);
}
/// <summary>
@@ -135,30 +131,21 @@ namespace MediaBrowser.Providers.Studios
/// </summary>
/// <param name="url">The URL.</param>
/// <param name="file">The file.</param>
- /// <param name="httpClient">The HTTP client.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public async Task<string> EnsureList(string url, string file, IHttpClient httpClient, IFileSystem fileSystem, CancellationToken cancellationToken)
+ public async Task<string> EnsureList(string url, string file, IFileSystem fileSystem, CancellationToken cancellationToken)
{
var fileInfo = fileSystem.GetFileInfo(file);
if (!fileInfo.Exists || (DateTime.UtcNow - fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays > 1)
{
- Directory.CreateDirectory(Path.GetDirectoryName(file));
+ var httpClient = _httpClientFactory.CreateClient();
- using (var res = await httpClient.SendAsync(
- new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (var content = res.Content)
- using (var fileStream = new FileStream(file, FileMode.Create))
- {
- await content.CopyToAsync(fileStream).ConfigureAwait(false);
- }
+ Directory.CreateDirectory(Path.GetDirectoryName(file));
+ await using var response = await httpClient.GetStreamAsync(url).ConfigureAwait(false);
+ await using var fileStream = new FileStream(file, FileMode.Create);
+ await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
return file;
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 3510b90cf..0f7cb3f8f 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Subtitles
CancellationToken cancellationToken)
{
var parts = subtitleId.Split(new[] { '_' }, 2);
- var provider = GetProvider(parts.First());
+ var provider = GetProvider(parts[0]);
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 4181d37ef..a2c0e62c1 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.TV
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error in DummySeasonProvider");
+ Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path);
}
}
diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs
deleted file mode 100644
index 63cbfd9e4..000000000
--- a/MediaBrowser.WebDashboard/Api/DashboardService.cs
+++ /dev/null
@@ -1,340 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.WebDashboard.Api
-{
- /// <summary>
- /// Class GetDashboardConfigurationPages.
- /// </summary>
- [Route("/web/ConfigurationPages", "GET")]
- public class GetDashboardConfigurationPages : IReturn<List<ConfigurationPageInfo>>
- {
- /// <summary>
- /// Gets or sets the type of the page.
- /// </summary>
- /// <value>The type of the page.</value>
- public ConfigurationPageType? PageType { get; set; }
-
- public bool? EnableInMainMenu { get; set; }
- }
-
- /// <summary>
- /// Class GetDashboardConfigurationPage.
- /// </summary>
- [Route("/web/ConfigurationPage", "GET")]
- public class GetDashboardConfigurationPage
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
- }
-
- [Route("/robots.txt", "GET", IsHidden = true)]
- public class GetRobotsTxt
- {
- }
-
- /// <summary>
- /// Class GetDashboardResource.
- /// </summary>
- [Route("/web/{ResourceName*}", "GET", IsHidden = true)]
- public class GetDashboardResource
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string ResourceName { get; set; }
-
- /// <summary>
- /// Gets or sets the V.
- /// </summary>
- /// <value>The V.</value>
- public string V { get; set; }
- }
-
- [Route("/favicon.ico", "GET", IsHidden = true)]
- public class GetFavIcon
- {
- }
-
- /// <summary>
- /// Class DashboardService.
- /// </summary>
- public class DashboardService : IService, IRequiresRequest
- {
- /// <summary>
- /// Gets or sets the logger.
- /// </summary>
- /// <value>The logger.</value>
- private readonly ILogger<DashboardService> _logger;
-
- /// <summary>
- /// Gets or sets the HTTP result factory.
- /// </summary>
- /// <value>The HTTP result factory.</value>
- private readonly IHttpResultFactory _resultFactory;
- private readonly IServerApplicationHost _appHost;
- private readonly IConfiguration _appConfig;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IFileSystem _fileSystem;
- private readonly IResourceFileManager _resourceFileManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DashboardService" /> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="appConfig">The application configuration.</param>
- /// <param name="resourceFileManager">The resource file manager.</param>
- /// <param name="serverConfigurationManager">The server configuration manager.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="resultFactory">The result factory.</param>
- public DashboardService(
- ILogger<DashboardService> logger,
- IServerApplicationHost appHost,
- IConfiguration appConfig,
- IResourceFileManager resourceFileManager,
- IServerConfigurationManager serverConfigurationManager,
- IFileSystem fileSystem,
- IHttpResultFactory resultFactory)
- {
- _logger = logger;
- _appHost = appHost;
- _appConfig = appConfig;
- _resourceFileManager = resourceFileManager;
- _serverConfigurationManager = serverConfigurationManager;
- _fileSystem = fileSystem;
- _resultFactory = resultFactory;
- }
-
- /// <summary>
- /// Gets or sets the request context.
- /// </summary>
- /// <value>The request context.</value>
- public IRequest Request { get; set; }
-
- /// <summary>
- /// Gets the path of the directory containing the static web interface content, or null if the server is not
- /// hosting the web client.
- /// </summary>
- public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager);
-
- /// <summary>
- /// Gets the path of the directory containing the static web interface content.
- /// </summary>
- /// <param name="appConfig">The app configuration.</param>
- /// <param name="serverConfigManager">The server configuration manager.</param>
- /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
- public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
- {
- if (!appConfig.HostWebClient())
- {
- return null;
- }
-
- if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
- {
- return serverConfigManager.Configuration.DashboardSourcePath;
- }
-
- return serverConfigManager.ApplicationPaths.WebPath;
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetFavIcon request)
- {
- return Get(new GetDashboardResource
- {
- ResourceName = "favicon.ico"
- });
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public Task<object> Get(GetDashboardConfigurationPage request)
- {
- IPlugin plugin = null;
- Stream stream = null;
-
- var isJs = false;
- var isTemplate = false;
-
- var page = ServerEntryPoint.Instance.PluginConfigurationPages.FirstOrDefault(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase));
- if (page != null)
- {
- plugin = page.Plugin;
- stream = page.GetHtmlStream();
- }
-
- if (plugin == null)
- {
- var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, request.Name, StringComparison.OrdinalIgnoreCase));
- if (altPage != null)
- {
- plugin = altPage.Item2;
- stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
-
- isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
- isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
- }
- }
-
- if (plugin != null && stream != null)
- {
- if (isJs)
- {
- return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.js"), () => Task.FromResult(stream));
- }
-
- if (isTemplate)
- {
- return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
- }
-
- return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
- }
-
- throw new ResourceNotFoundException();
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public object Get(GetDashboardConfigurationPages request)
- {
- const string unavailableMessage = "The server is still loading. Please try again momentarily.";
-
- var instance = ServerEntryPoint.Instance;
-
- if (instance == null)
- {
- throw new InvalidOperationException(unavailableMessage);
- }
-
- var pages = instance.PluginConfigurationPages;
-
- if (pages == null)
- {
- throw new InvalidOperationException(unavailableMessage);
- }
-
- // Don't allow a failing plugin to fail them all
- var configPages = pages.Select(p =>
- {
- try
- {
- return new ConfigurationPageInfo(p);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name);
- return null;
- }
- })
- .Where(i => i != null)
- .ToList();
-
- configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
-
- if (request.PageType.HasValue)
- {
- configPages = configPages.Where(p => p.ConfigurationPageType == request.PageType.Value).ToList();
- }
-
- if (request.EnableInMainMenu.HasValue)
- {
- configPages = configPages.Where(p => p.EnableInMainMenu == request.EnableInMainMenu.Value).ToList();
- }
-
- return configPages;
- }
-
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
- {
- return _appHost.Plugins.SelectMany(GetPluginPages);
- }
-
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
- {
- var hasConfig = plugin as IHasWebPages;
-
- if (hasConfig == null)
- {
- return new List<Tuple<PluginPageInfo, IPlugin>>();
- }
-
- return hasConfig.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
- }
-
- private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
- {
- return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
- }
-
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
- public object Get(GetRobotsTxt request)
- {
- return Get(new GetDashboardResource
- {
- ResourceName = "robots.txt"
- });
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public async Task<object> Get(GetDashboardResource request)
- {
- if (!_appConfig.HostWebClient() || DashboardUIPath == null)
- {
- throw new ResourceNotFoundException();
- }
-
- var path = request?.ResourceName;
- var basePath = DashboardUIPath;
-
- // Bounce them to the startup wizard if it hasn't been completed yet
- if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
- && !Request.RawUrl.Contains("wizard", StringComparison.OrdinalIgnoreCase)
- && Request.RawUrl.Contains("index", StringComparison.OrdinalIgnoreCase))
- {
- Request.Response.Redirect("index.html?start=wizard#!/wizardstart.html");
- return null;
- }
-
- return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)).ConfigureAwait(false);
- }
- }
-}
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
deleted file mode 100644
index bcaee50f2..000000000
--- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
+++ /dev/null
@@ -1,42 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
- <PropertyGroup>
- <ProjectGuid>{5624B7B5-B5A7-41D8-9F10-CC5611109619}</ProjectGuid>
- </PropertyGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
- </ItemGroup>
-
- <ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
- </ItemGroup>
-
- <ItemGroup>
- <None Include="jellyfin-web\**\*.*">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </None>
- </ItemGroup>
-
- <PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- </PropertyGroup>
-
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
- </ItemGroup>
-
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
-</Project>
diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs
deleted file mode 100644
index 584d49021..000000000
--- a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("MediaBrowser.WebDashboard")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin Server")]
-[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
diff --git a/MediaBrowser.WebDashboard/ServerEntryPoint.cs b/MediaBrowser.WebDashboard/ServerEntryPoint.cs
deleted file mode 100644
index 5c7e8b3c7..000000000
--- a/MediaBrowser.WebDashboard/ServerEntryPoint.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Controller.Plugins;
-
-namespace MediaBrowser.WebDashboard
-{
- public sealed class ServerEntryPoint : IServerEntryPoint
- {
- private readonly IApplicationHost _appHost;
-
- public ServerEntryPoint(IApplicationHost appHost)
- {
- _appHost = appHost;
- Instance = this;
- }
-
- public static ServerEntryPoint Instance { get; private set; }
-
- /// <summary>
- /// Gets the list of plugin configuration pages.
- /// </summary>
- /// <value>The configuration pages.</value>
- public List<IPluginConfigurationPage> PluginConfigurationPages { get; private set; }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- PluginConfigurationPages = _appHost.GetExports<IPluginConfigurationPage>().ToList();
-
- return Task.CompletedTask;
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- }
- }
-}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index b2d99c1a1..b06464409 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -222,8 +222,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (index != -1)
{
- var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0];
- if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
+ index = tmdbId.IndexOf('-');
+ if (index != -1)
+ {
+ tmdbId = tmdbId.Slice(0, index);
+ }
+
+ if (!tmdbId.IsEmpty
+ && !tmdbId.IsWhiteSpace()
+ && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture));
}
@@ -237,8 +245,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (index != -1)
{
- var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
- if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
+ if (!tvdbId.IsEmpty
+ && !tvdbId.IsWhiteSpace()
+ && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture));
}
@@ -442,8 +452,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- var hasAspectRatio = item as IHasAspectRatio;
- if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null)
+ if (!string.IsNullOrWhiteSpace(val)
+ && item is IHasAspectRatio hasAspectRatio)
{
hasAspectRatio.AspectRatio = val;
}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index e100c0b1c..75587da1f 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -6,14 +6,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}"
@@ -82,10 +78,6 @@ Global
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU
- {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -94,10 +86,6 @@ Global
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU
{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/README.md b/README.md
index 83090100d..55d6917ae 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@ Note that it is also possible to [host the web client separately](#hosting-the-w
There are three options to get the files for the web client.
-1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
+1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
@@ -162,6 +162,9 @@ The following sections describe some more advanced scenarios for running the ser
It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this.
-To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
+To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line
+switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
+
+**NOTE:** The setup wizard can not be run if the web client is hosted separately.
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index 553693171..664663bd7 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -6,9 +6,7 @@
</PropertyGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
</ItemGroup>
<PropertyGroup>
diff --git a/SharedVersion.cs b/SharedVersion.cs
index 6981c1ca9..d17074fc0 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.6.0")]
-[assembly: AssemblyFileVersion("10.6.0")]
+[assembly: AssemblyVersion("10.7.0")]
+[assembly: AssemblyFileVersion("10.7.0")]
diff --git a/build.yaml b/build.yaml
index 9e590e5a0..7c32d955c 100644
--- a/build.yaml
+++ b/build.yaml
@@ -1,7 +1,7 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
-version: "10.6.0"
+version: "10.7.0"
packages:
- debian.amd64
- debian.arm64
diff --git a/bump_version b/bump_version
index 46b7f86e0..1c943f691 100755
--- a/bump_version
+++ b/bump_version
@@ -4,6 +4,7 @@
set -o errexit
set -o pipefail
+set -o xtrace
usage() {
echo -e "bump_version - increase the shared version and generate changelogs"
@@ -58,7 +59,7 @@ sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file}
debian_changelog_file="debian/changelog"
debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog
-echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium
+echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
diff --git a/debian/changelog b/debian/changelog
index 35fb65957..184143fe9 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
+jellyfin-server (10.7.0-1) unstable; urgency=medium
+
+ * Forthcoming stable release
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:09:45 -0400
+
+jellyfin-server (10.6.0-2) unstable; urgency=medium
+
+ * Fix upgrade bug
+
+ -- Joshua Boniface <joshua@boniface.me> Sun, 19 Jul 22:47:27 -0400
+
jellyfin-server (10.6.0-1) unstable; urgency=medium
* Forthcoming stable release
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index 64c98520c..7cbfa88ee 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -31,7 +31,7 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
#JELLYFIN_SERVICE_OPT="--service"
# [OPTIONAL] run Jellyfin without the web app
-#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
+#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
#
# SysV init/Upstart options
@@ -40,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# Application username
JELLYFIN_USER="jellyfin"
# Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT"
diff --git a/debian/control b/debian/control
index 896d8286b..39c2aa055 100644
--- a/debian/control
+++ b/debian/control
@@ -15,9 +15,8 @@ Vcs-Git: https://github.org/jellyfin/jellyfin.git
Vcs-Browser: https://github.org/jellyfin/jellyfin
Package: jellyfin-server
-Replaces: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
-Breaks: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
-Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
+Replaces: jellyfin (<<10.6.0)
+Breaks: jellyfin (<<10.6.0)
Architecture: any
Depends: at,
libsqlite3-0,
diff --git a/debian/metapackage/jellyfin b/debian/metapackage/jellyfin
index 9a41eafae..026fcb821 100644
--- a/debian/metapackage/jellyfin
+++ b/debian/metapackage/jellyfin
@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
Standards-Version: 3.9.2
Package: jellyfin
-Version: 10.6.0
+Version: 10.7.0
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
Depends: jellyfin-server, jellyfin-web
Description: Provides the Jellyfin Free Software Media System
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index b5a038048..f9c6a1674 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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.debian.arm64 b/deployment/Dockerfile.debian.arm64
index cfe562df3..5c08444df 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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.debian.armhf b/deployment/Dockerfile.debian.armhf
index ea8c8c8e6..b54fc3f91 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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.linux.amd64 b/deployment/Dockerfile.linux.amd64
index d8bec9214..3a2f67615 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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.macos b/deployment/Dockerfile.macos
index ba5da4019..d1c0784ff 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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.portable b/deployment/Dockerfile.portable
index 2893e140d..7270188fd 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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 e61be4efc..b7d3b4bde 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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 f91b91cd4..dc90f9fbd 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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 85414614c..db98610c9 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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.windows.amd64 b/deployment/Dockerfile.windows.amd64
index 0397a023e..95fd10cf4 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-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/build.windows.amd64 b/deployment/build.windows.amd64
index 3fabc2cac..bd5dc6438 100755
--- a/deployment/build.windows.amd64
+++ b/deployment/build.windows.amd64
@@ -8,8 +8,7 @@ set -o xtrace
# Version variables
NSSM_VERSION="nssm-2.24-101-g897c7ad"
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.2.1-win64-static"
-FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
+FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip";
# Move to source directory
pushd ${SOURCE_DIR}
@@ -29,12 +28,11 @@ dotnet publish Jellyfin.Server --configuration Release --self-contained --runtim
# Prepare addins
addin_build_dir="$( mktemp -d )"
wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
-wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip
+wget ${FFMPEG_URL} -O ${addin_build_dir}/jellyfin-ffmpeg.zip
unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe
-unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir}
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${output_dir}/ffmpeg.exe
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe ${output_dir}/ffprobe.exe
+unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmpeg
+cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir}
rm -rf ${addin_build_dir}
# Prepare scripts
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 9311864a6..b0a729a9e 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -7,7 +7,7 @@
%endif
Name: jellyfin
-Version: 10.6.0
+Version: 10.7.0
Release: 1%{?dist}
Summary: The Free Software Media System
License: GPLv3
@@ -139,6 +139,8 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
+* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
+- Forthcoming stable release
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release
* Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index a0f36ebbf..4ea5094b6 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -132,7 +132,7 @@ namespace Jellyfin.Api.Tests.Auth
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>()))
- .Returns(authorizationInfo);
+ .Returns(authorizationInfo);
return authorizationInfo;
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
new file mode 100644
index 000000000..a62fd8d5a
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
+{
+ public class DefaultAuthorizationHandlerTests
+ {
+ private readonly Mock<IConfigurationManager> _configurationManagerMock;
+ private readonly List<IAuthorizationRequirement> _requirements;
+ private readonly DefaultAuthorizationHandler _sut;
+ private readonly Mock<IUserManager> _userManagerMock;
+ private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+
+ public DefaultAuthorizationHandlerTests()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+ _requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement() };
+ _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+ _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+ _sut = fixture.Create<DefaultAuthorizationHandler>();
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator)]
+ [InlineData(UserRoles.Guest)]
+ [InlineData(UserRoles.User)]
+ public async Task ShouldSucceedOnUser(string userRole)
+ {
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ userRole);
+
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
+
+ await _sut.HandleAsync(context);
+ Assert.True(context.HasSucceeded);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
index e40af703f..ee42216e4 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
@@ -1,13 +1,13 @@
using System.Collections.Generic;
-using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
+using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
@@ -18,12 +18,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly FirstTimeSetupOrElevatedHandler _sut;
+ private readonly Mock<IUserManager> _userManagerMock;
+ private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public FirstTimeSetupOrElevatedHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
+ _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+ _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
}
@@ -34,9 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User)]
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
{
- SetupConfigurationManager(false);
- var user = SetupUser(userRole);
- var context = new AuthorizationHandlerContext(_requirements, user, null);
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ userRole);
+
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
@@ -48,30 +56,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User, false)]
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
{
- SetupConfigurationManager(true);
- var user = SetupUser(userRole);
- var context = new AuthorizationHandlerContext(_requirements, user, null);
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ userRole);
+
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
-
- private static ClaimsPrincipal SetupUser(string role)
- {
- var claims = new[] { new Claim(ClaimTypes.Role, role) };
- var identity = new ClaimsIdentity(claims);
- return new ClaimsPrincipal(identity);
- }
-
- private void SetupConfigurationManager(bool startupWizardCompleted)
- {
- var commonConfiguration = new BaseApplicationConfiguration
- {
- IsStartupWizardCompleted = startupWizardCompleted
- };
-
- _configurationManagerMock.Setup(c => c.CommonConfiguration)
- .Returns(commonConfiguration);
- }
}
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
new file mode 100644
index 000000000..7150c90bb
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
+{
+ public class IgnoreScheduleHandlerTests
+ {
+ private readonly Mock<IConfigurationManager> _configurationManagerMock;
+ private readonly List<IAuthorizationRequirement> _requirements;
+ private readonly IgnoreParentalControlHandler _sut;
+ private readonly Mock<IUserManager> _userManagerMock;
+ private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+
+ /// <summary>
+ /// Globally disallow access.
+ /// </summary>
+ private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
+
+ public IgnoreScheduleHandlerTests()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+ _requirements = new List<IAuthorizationRequirement> { new IgnoreParentalControlRequirement() };
+ _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+ _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+ _sut = fixture.Create<IgnoreParentalControlHandler>();
+ }
+
+ [Theory]
+ [InlineData(UserRoles.Administrator, true)]
+ [InlineData(UserRoles.User, true)]
+ [InlineData(UserRoles.Guest, true)]
+ public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed)
+ {
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ role,
+ _accessSchedules);
+
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
+
+ await _sut.HandleAsync(context);
+ Assert.Equal(shouldSucceed, context.HasSucceeded);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
new file mode 100644
index 000000000..09ffa8468
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
+{
+ public class LocalAccessHandlerTests
+ {
+ private readonly Mock<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<string>()))
+ .Returns(isInLocalNetwork);
+
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ UserRoles.User);
+
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
+ await _sut.HandleAsync(context);
+ Assert.Equal(shouldSucceed, context.HasSucceeded);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
index cd05a8328..ffe88fcde 100644
--- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
@@ -1,20 +1,35 @@
using System.Collections.Generic;
-using System.Security.Claims;
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()
{
- _sut = new RequiresElevationHandler();
+ 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]
@@ -23,13 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
[InlineData(UserRoles.Guest, false)]
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
{
- var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
-
- var claims = new[] { new Claim(ClaimTypes.Role, role) };
- var identity = new ClaimsIdentity(claims);
- var user = new ClaimsPrincipal(identity);
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ role);
- var context = new AuthorizationHandlerContext(requirements, user, null);
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
diff --git a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
deleted file mode 100644
index b01d1af1f..000000000
--- a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using MediaBrowser.Api;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using Xunit;
-
-namespace Jellyfin.Api.Tests
-{
- public class GetPathValueTests
- {
- [Theory]
- [InlineData("https://localhost:8096/ScheduledTasks/1234/Triggers", "", 1, "1234")]
- [InlineData("https://localhost:8096/emby/ScheduledTasks/1234/Triggers", "", 1, "1234")]
- [InlineData("https://localhost:8096/mediabrowser/ScheduledTasks/1234/Triggers", "", 1, "1234")]
- [InlineData("https://localhost:8096/jellyfin/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
- [InlineData("https://localhost:8096/jellyfin/2/emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
- [InlineData("https://localhost:8096/jellyfin/2/mediabrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
- [InlineData("https://localhost:8096/JELLYFIN/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
- [InlineData("https://localhost:8096/JELLYFIN/2/Emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
- [InlineData("https://localhost:8096/JELLYFIN/2/MediaBrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
- public void GetPathValueTest(string path, string baseUrl, int index, string value)
- {
- var reqMock = Mock.Of<IRequest>(x => x.PathInfo == path);
- var conf = new ServerConfiguration()
- {
- BaseUrl = baseUrl
- };
-
- var confManagerMock = Mock.Of<IServerConfigurationManager>(x => x.Configuration == conf);
-
- var service = new BrandingService(
- new NullLogger<BrandingService>(),
- confManagerMock,
- Mock.Of<IHttpResultFactory>())
- {
- Request = reqMock
- };
-
- Assert.Equal(value, service.GetPathValue(index).ToString());
- }
- }
-}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 100f95424..f77eba376 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -13,15 +13,15 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.12.0" />
+ <PackageReference Include="AutoFixture" Version="4.13.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
- <PackageReference Include="Moq" Version="4.14.4" />
+ <PackageReference Include="Moq" Version="4.14.5" />
</ItemGroup>
<!-- Code Analyzers -->
@@ -33,8 +33,7 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
- <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
+ <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
new file mode 100644
index 000000000..a4dd4e409
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
+
+namespace Jellyfin.Api.Tests
+{
+ public static class TestHelpers
+ {
+ public static ClaimsPrincipal SetupUser(
+ Mock<IUserManager> userManagerMock,
+ Mock<IHttpContextAccessor> httpContextAccessorMock,
+ string role,
+ IEnumerable<AccessSchedule>? accessSchedules = null)
+ {
+ var user = new User(
+ "jellyfin",
+ typeof(DefaultAuthenticationProvider).FullName,
+ typeof(DefaultPasswordResetProvider).FullName);
+
+ // Set administrator flag.
+ user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
+
+ // Add access schedules if set.
+ if (accessSchedules != null)
+ {
+ foreach (var accessSchedule in accessSchedules)
+ {
+ user.AccessSchedules.Add(accessSchedule);
+ }
+ }
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.Role, role),
+ new Claim(ClaimTypes.Name, "jellyfin"),
+ new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.Device, "test"),
+ new Claim(InternalClaimTypes.Client, "test"),
+ new Claim(InternalClaimTypes.Version, "test"),
+ new Claim(InternalClaimTypes.Token, "test"),
+ };
+
+ var identity = new ClaimsIdentity(claims);
+
+ userManagerMock
+ .Setup(u => u.GetUserById(It.IsAny<Guid>()))
+ .Returns(user);
+
+ httpContextAccessorMock
+ .Setup(h => h.HttpContext.Connection.RemoteIpAddress)
+ .Returns(new IPAddress(0));
+
+ return new ClaimsPrincipal(identity);
+ }
+
+ public static void SetupConfigurationManager(in Mock<IConfigurationManager> configurationManagerMock, bool startupWizardCompleted)
+ {
+ var commonConfiguration = new BaseApplicationConfiguration
+ {
+ IsStartupWizardCompleted = startupWizardCompleted
+ };
+
+ configurationManagerMock
+ .Setup(c => c.CommonConfiguration)
+ .Returns(commonConfiguration);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 4cb1da994..746474044 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 18724f31c..1559f70ab 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index a9e3cf7f0..c6b9cf67c 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -18,6 +18,7 @@ namespace Jellyfin.MediaEncoding.Tests
}
[Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)]
@@ -33,6 +34,7 @@ namespace Jellyfin.MediaEncoding.Tests
{
public IEnumerator<object?[]> GetEnumerator()
{
+ yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index c46c9578b..f5ff3d723 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
+ public const string FFmpegV43Output = @"ffmpeg version 4.3 Copyright (c) 2000-2020 the FFmpeg developers
+built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
+configuration: --prefix=/usr/lib/jellyfin-ffmpeg --target-os=linux --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-vdpau --disable-sdl2 --disable-xlib --enable-gpl --enable-version3 --enable-static --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --arch=amd64 --enable-amf --enable-nvenc --enable-nvdec --enable-vaapi --enable-opencl
+libavutil 56. 51.100 / 56. 51.100
+libavcodec 58. 91.100 / 58. 91.100
+libavformat 58. 45.100 / 58. 45.100
+libavdevice 58. 10.100 / 58. 10.100
+libavfilter 7. 85.100 / 7. 85.100
+libswscale 5. 7.100 / 5. 7.100
+libswresample 3. 7.100 / 3. 7.100
+libpostproc 55. 7.100 / 55. 7.100";
+
public const string FFmpegV421Output = @"ffmpeg version 4.2.1 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 9.1.1 (GCC) 20190807
configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 646ef00fd..e1a089547 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 1434cce96..0e9e91563 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
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 dd53c9902..03187f4b9 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -14,9 +14,9 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.12.0" />
+ <PackageReference Include="AutoFixture" Version="4.13.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
- <PackageReference Include="Moq" Version="4.14.4" />
+ <PackageReference Include="Moq" Version="4.14.5" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
index c145ddc9d..b4e6db8f3 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
@@ -9,17 +9,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/small.jpg", true)]
[InlineData("/media/albumart.jpg", true)]
[InlineData("/media/movie.sample.mp4", true)]
+ [InlineData("/media/movie/sample.mp4", true)]
+ [InlineData("/media/movie/sample/movie.mp4", true)]
+ [InlineData("/foo/sample/bar/baz.mkv", false)]
+ [InlineData("/media/movies/the sample/the sample.mkv", false)]
+ [InlineData("/media/movies/sampler.mkv", false)]
[InlineData("/media/movies/#Recycle/test.txt", true)]
[InlineData("/media/movies/#recycle/", true)]
[InlineData("/media/movies/#recycle", true)]
[InlineData("thumbs.db", true)]
[InlineData(@"C:\media\movies\movie.avi", false)]
- [InlineData("/media/.hiddendir/file.mp4", true)]
+ [InlineData("/media/.hiddendir/file.mp4", false)]
[InlineData("/media/dir/.hiddenfile.mp4", true)]
+ [InlineData("/media/dir/._macjunk.mp4", true)]
[InlineData("/volume1/video/Series/@eaDir", true)]
[InlineData("/volume1/video/Series/@eaDir/file.txt", true)]
[InlineData("/directory/@Recycle", true)]
[InlineData("/directory/@Recycle/file.mp3", true)]
+ [InlineData("/media/movies/.@__thumb", true)]
+ [InlineData("/media/movies/.@__thumb/foo-bar-thumbnail.png", true)]
+ [InlineData("/media/music/Foo B.A.R./epic.flac", false)]
+ [InlineData("/media/music/Foo B.A.R", false)]
+ // This test is pending an upstream fix: https://github.com/dazinator/DotNet.Glob/issues/78
+ // [InlineData("/media/music/Foo B.A.R.", false)]
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
diff --git a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
index 34698fe25..5d7f7765c 100644
--- a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
+++ b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
@@ -43,7 +43,7 @@ namespace MediaBrowser.Api.Tests
// Assert
response.EnsureSuccessStatusCode();
- Assert.Equal("text/css", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType.ToString());
}
}
}
diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
index a767c0039..a4ef10648 100644
--- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
+++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.5" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.6" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
@@ -17,7 +17,6 @@
<ItemGroup>
<ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
- <ProjectReference Include="..\..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1
index c762137a7..b76a8e0bb 100644
--- a/windows/build-jellyfin.ps1
+++ b/windows/build-jellyfin.ps1
@@ -47,7 +47,7 @@ function Install-FFMPEG {
param(
[string]$ResolvedInstallLocation,
[string]$Architecture,
- [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared"
+ [string]$FFMPEGVersionX86 = "ffmpeg-4.3-win32-shared"
)
Write-Verbose "Checking Architecture"
if($Architecture -notin @('x86','x64')){