aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrobibero <cody@robibe.ro>2020-06-24 10:52:39 -0600
committercrobibero <cody@robibe.ro>2020-06-24 10:52:39 -0600
commitcbcf3bfaffadf9d467b2bd0bb7f3e834d13d56d3 (patch)
tree60c33f03ed003794eb61058ff870dc4f2dbee64d
parent8f1505cdf5bc964c294cf0575807233f0e7af7d7 (diff)
parent6ad50f2e023cdf8fedce8c828c507576d590858e (diff)
Merge remote-tracking branch 'upstream/api-migration' into api-channel
-rw-r--r--CONTRIBUTORS.md2
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectory.cs8
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs33
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs14
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs10
-rw-r--r--Emby.Drawing/ImageProcessor.cs9
-rw-r--r--Emby.Notifications/NotificationManager.cs8
-rw-r--r--Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs56
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs14
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs11
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs1
-rw-r--r--Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs1
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserRepository.cs240
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs29
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs16
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj1
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs1
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs3
-rw-r--r--Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs77
-rw-r--r--Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs28
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs1
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs65
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs115
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs10
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs9
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs47
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs2
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs18
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs3
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs1107
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs25
-rw-r--r--Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs3
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs3
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs82
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs1
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs79
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs14
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs100
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs24
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs22
-rw-r--r--Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.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.cs15
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs5
-rw-r--r--Jellyfin.Api/Controllers/BrandingController.cs57
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs26
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs275
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs21
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs76
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs221
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs230
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs364
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs90
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs (renamed from MediaBrowser.Api/ItemUpdateService.cs)425
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs341
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs76
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs51
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs38
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs199
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs200
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs266
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs475
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs44
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs347
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs87
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs222
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs380
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs552
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs202
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs162
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs75
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs65
-rw-r--r--Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs2
-rw-r--r--Jellyfin.Api/Models/ConfigurationPageInfo.cs (renamed from MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs)38
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs2
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs2
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs2
-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/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.Data/DayOfWeekHelper.cs65
-rw-r--r--Jellyfin.Data/Entities/AccessSchedule.cs91
-rw-r--r--Jellyfin.Data/Entities/Group.cs103
-rw-r--r--Jellyfin.Data/Entities/ImageInfo.cs30
-rw-r--r--Jellyfin.Data/Entities/Permission.cs137
-rw-r--r--Jellyfin.Data/Entities/Preference.cs100
-rw-r--r--Jellyfin.Data/Entities/ProviderMapping.cs6
-rw-r--r--Jellyfin.Data/Entities/Series.cs20
-rw-r--r--Jellyfin.Data/Entities/User.cs500
-rw-r--r--Jellyfin.Data/Enums/DynamicDayOfWeek.cs (renamed from MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs)2
-rw-r--r--Jellyfin.Data/Enums/PermissionKind.cs127
-rw-r--r--Jellyfin.Data/Enums/PreferenceKind.cs69
-rw-r--r--Jellyfin.Data/Enums/SubtitlePlaybackMode.cs (renamed from MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs)4
-rw-r--r--Jellyfin.Data/Enums/SyncPlayAccess.cs (renamed from MediaBrowser.Model/Configuration/SyncplayAccess.cs)8
-rw-r--r--Jellyfin.Data/Enums/UnratedItem.cs (renamed from MediaBrowser.Model/Configuration/UnratedItem.cs)2
-rw-r--r--Jellyfin.Data/Enums/Weekday.cs13
-rw-r--r--Jellyfin.Data/IHasPermissions.cs31
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj1
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj2
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs70
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs312
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.cs197
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs244
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs (renamed from Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs)16
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs (renamed from Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs)59
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs67
-rw-r--r--Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs (renamed from Emby.Server.Implementations/Library/InvalidAuthProvider.cs)12
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs841
-rw-r--r--Jellyfin.Server/CoreAppHost.cs13
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs62
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs208
-rw-r--r--Jellyfin.Server/Program.cs4
-rw-r--r--MediaBrowser.Api/BaseApiService.cs5
-rw-r--r--MediaBrowser.Api/BrandingService.cs44
-rw-r--r--MediaBrowser.Api/DisplayPreferencesService.cs101
-rw-r--r--MediaBrowser.Api/FilterService.cs243
-rw-r--r--MediaBrowser.Api/Images/ImageByNameService.cs277
-rw-r--r--MediaBrowser.Api/Images/ImageService.cs160
-rw-r--r--MediaBrowser.Api/Images/RemoteImageService.cs297
-rw-r--r--MediaBrowser.Api/ItemLookupService.cs336
-rw-r--r--MediaBrowser.Api/ItemRefreshService.cs83
-rw-r--r--MediaBrowser.Api/Library/LibraryService.cs16
-rw-r--r--MediaBrowser.Api/Library/LibraryStructureService.cs412
-rw-r--r--MediaBrowser.Api/LiveTv/LiveTvService.cs3
-rw-r--r--MediaBrowser.Api/LocalizationService.cs111
-rw-r--r--MediaBrowser.Api/Movies/MoviesService.cs11
-rw-r--r--MediaBrowser.Api/Music/InstantMixService.cs1
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs3
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs36
-rw-r--r--MediaBrowser.Api/PlaylistService.cs217
-rw-r--r--MediaBrowser.Api/PluginService.cs268
-rw-r--r--MediaBrowser.Api/Sessions/SessionService.cs498
-rw-r--r--MediaBrowser.Api/Subtitles/SubtitleService.cs300
-rw-r--r--MediaBrowser.Api/SuggestionsService.cs97
-rw-r--r--MediaBrowser.Api/System/SystemService.cs226
-rw-r--r--MediaBrowser.Api/TestService.cs26
-rw-r--r--MediaBrowser.Api/TvShowsService.cs498
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs1
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs19
-rw-r--r--MediaBrowser.Api/UserLibrary/PlaystateService.cs2
-rw-r--r--MediaBrowser.Api/UserLibrary/UserLibraryService.cs2
-rw-r--r--MediaBrowser.Api/UserService.cs600
-rw-r--r--MediaBrowser.Api/VideosService.cs193
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs37
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs1
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs2
-rw-r--r--MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs2
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs10
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs1
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs3
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs1
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs1
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs16
-rw-r--r--MediaBrowser.Controller/Entities/AudioBook.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs48
-rw-r--r--MediaBrowser.Controller/Entities/Book.cs2
-rw-r--r--MediaBrowser.Controller/Entities/DayOfWeekHelper.cs71
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs28
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs14
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs8
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs2
-rw-r--r--MediaBrowser.Controller/Entities/MusicVideo.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs10
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs29
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs2
-rw-r--r--MediaBrowser.Controller/Entities/User.cs262
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs1
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs19
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs53
-rw-r--r--MediaBrowser.Controller/Library/IIntroProvider.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs11
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs1
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs1
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs3
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs130
-rw-r--r--MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs17
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs10
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs1
-rw-r--r--MediaBrowser.Controller/Net/IAuthService.cs28
-rw-r--r--MediaBrowser.Controller/Net/IAuthorizationContext.cs11
-rw-r--r--MediaBrowser.Controller/Net/ISessionContext.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationService.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/UserNotification.cs2
-rw-r--r--MediaBrowser.Controller/Persistence/IUserRepository.cs27
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs11
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs3
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs3
-rw-r--r--MediaBrowser.Model/Configuration/AccessSchedule.cs2
-rw-r--r--MediaBrowser.Model/Configuration/UserConfiguration.cs1
-rw-r--r--MediaBrowser.Model/Entities/ImageType.cs7
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOptions.cs7
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs6
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs24
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs14
-rw-r--r--MediaBrowser.Providers/Users/UserMetadataService.cs30
-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.sln6
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs73
-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.cs4
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj13
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs81
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj12
-rw-r--r--tests/Jellyfin.Common.Tests/PasswordHashTests.cs3
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs28
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj12
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj12
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj5
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj48
-rw-r--r--tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs2
-rw-r--r--tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj4
254 files changed, 10291 insertions, 8633 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ce956176e..c5f35c088 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -7,6 +7,7 @@
- [anthonylavado](https://github.com/anthonylavado)
- [Artiume](https://github.com/Artiume)
- [AThomsen](https://github.com/AThomsen)
+ - [barronpm](https://github.com/barronpm)
- [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers)
- [BnMcG](https://github.com/BnMcG)
@@ -130,6 +131,7 @@
- [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [KristupasSavickas](https://github.com/KristupasSavickas)
+ - [Pusta](https://github.com/pusta)
# Emby Contributors
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs
index 66805b7c8..b1ce7e8ec 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs
@@ -4,11 +4,12 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Emby.Dlna.Service;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.TV;
@@ -32,7 +33,8 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager;
- public ContentDirectory(IDlnaManager dlna,
+ public ContentDirectory(
+ IDlnaManager dlna,
IUserDataManager userDataManager,
IImageProcessor imageProcessor,
ILibraryManager libraryManager,
@@ -131,7 +133,7 @@ namespace Emby.Dlna.ContentDirectory
foreach (var user in _userManager.Users)
{
- if (user.Policy.IsAdministrator)
+ if (user.HasPermission(PermissionKind.IsAdministrator))
{
return user;
}
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 28888f031..27585eafa 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -10,6 +10,7 @@ using System.Threading;
using System.Xml;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
@@ -17,7 +18,6 @@ using MediaBrowser.Controller.Dto;
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.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
@@ -28,6 +28,12 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory
{
@@ -731,7 +737,7 @@ namespace Emby.Dlna.ContentDirectory
return GetGenres(item, user, query);
}
- var array = new ServerItem[]
+ var array = new[]
{
new ServerItem(item)
{
@@ -1115,7 +1121,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { typeof(Playlist).Name };
+ query.IncludeItemTypes = new[] { nameof(Playlist) };
query.SetUser(user);
query.Recursive = true;
@@ -1132,10 +1138,9 @@ namespace Emby.Dlna.ContentDirectory
{
UserId = user.Id,
Limit = 50,
- IncludeItemTypes = new[] { typeof(Audio).Name },
- ParentId = parent == null ? Guid.Empty : parent.Id,
+ IncludeItemTypes = new[] { nameof(Audio) },
+ ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
-
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
@@ -1150,7 +1155,6 @@ namespace Emby.Dlna.ContentDirectory
Limit = query.Limit,
StartIndex = query.StartIndex,
UserId = query.User.Id
-
}, new[] { parent }, query.DtoOptions);
return ToResult(result);
@@ -1167,7 +1171,6 @@ namespace Emby.Dlna.ContentDirectory
IncludeItemTypes = new[] { typeof(Episode).Name },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
-
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
@@ -1177,14 +1180,14 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
- var items = _userViewManager.GetLatestItems(new LatestItemsQuery
+ var items = _userViewManager.GetLatestItems(
+ new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
- IncludeItemTypes = new[] { typeof(Movie).Name },
- ParentId = parent == null ? Guid.Empty : parent.Id,
+ IncludeItemTypes = new[] { nameof(Movie) },
+ ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
-
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
@@ -1217,7 +1220,11 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name },
+ IncludeItemTypes = new[]
+ {
+ nameof(Movie),
+ nameof(Series)
+ },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index f7d840c62..6cedb3ef0 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -6,14 +6,13 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
-using Emby.Dlna.Configuration;
using Emby.Dlna.ContentDirectory;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
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.MediaEncoding;
using MediaBrowser.Controller.Playlists;
@@ -23,6 +22,13 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
+using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
namespace Emby.Dlna.Didl
{
@@ -421,7 +427,6 @@ namespace Emby.Dlna.Didl
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
case StubType.Series: return _localization.GetLocalizedString("Shows");
- default: break;
}
}
@@ -670,7 +675,7 @@ namespace Emby.Dlna.Didl
return;
}
- MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
+ XmlAttribute secAttribute = null;
foreach (var attribute in _profile.XmlRootAttributes)
{
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -995,7 +1000,6 @@ namespace Emby.Dlna.Didl
}
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
-
}
private void AddImageResElement(
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 7403a2a16..f1c69196a 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.Didl;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
namespace Emby.Dlna.PlayTo
{
@@ -446,7 +448,13 @@ namespace Emby.Dlna.PlayTo
}
}
- private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+ private PlaylistItem CreatePlaylistItem(
+ BaseItem item,
+ User user,
+ long startPostionTicks,
+ string mediaSourceId,
+ int? audioStreamIndex,
+ int? subtitleStreamIndex)
{
var deviceInfo = _device.Properties;
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 5b0f004d1..8696cb280 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
@@ -14,6 +15,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
namespace Emby.Drawing
{
@@ -349,6 +351,13 @@ namespace Emby.Drawing
});
}
+ /// <inheritdoc />
+ public string GetImageCacheTag(User user)
+ {
+ return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
+ .ToString("N", CultureInfo.InvariantCulture);
+ }
+
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{
var inputFormat = Path.GetExtension(originalImagePath)
diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs
index 2792a8f0b..8b281e487 100644
--- a/Emby.Notifications/NotificationManager.cs
+++ b/Emby.Notifications/NotificationManager.cs
@@ -4,6 +4,8 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@@ -101,7 +103,7 @@ namespace Emby.Notifications
switch (request.SendToUserMode.Value)
{
case SendToUserType.Admins:
- return _userManager.Users.Where(i => i.Policy.IsAdministrator)
+ return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
.Select(i => i.Id);
case SendToUserType.All:
return _userManager.UsersIds;
@@ -117,7 +119,7 @@ namespace Emby.Notifications
var config = GetConfiguration();
return _userManager.Users
- .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i.Policy))
+ .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i))
.Select(i => i.Id);
}
@@ -142,7 +144,7 @@ namespace Emby.Notifications
User = user
};
- _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Name);
+ _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username);
try
{
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
index 6917efefa..84bec9201 100644
--- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
+++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
@@ -88,25 +88,26 @@ namespace Emby.Server.Implementations.Activity
_subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
- _userManager.UserCreated += OnUserCreated;
- _userManager.UserPasswordChanged += OnUserPasswordChanged;
- _userManager.UserDeleted += OnUserDeleted;
- _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
- _userManager.UserLockedOut += OnUserLockedOut;
+ _userManager.OnUserCreated += OnUserCreated;
+ _userManager.OnUserPasswordChanged += OnUserPasswordChanged;
+ _userManager.OnUserDeleted += OnUserDeleted;
+ _userManager.OnUserLockedOut += OnUserLockedOut;
return Task.CompletedTask;
}
- private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+ private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserLockedOutWithName"),
- e.Argument.Name),
+ e.Argument.Username),
NotificationType.UserLockedOut.ToString(),
- e.Argument.Id))
- .ConfigureAwait(false);
+ e.Argument.Id)
+ {
+ LogSeverity = LogLevel.Error
+ }).ConfigureAwait(false);
}
private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
@@ -152,7 +153,7 @@ namespace Emby.Server.Implementations.Activity
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
- user.Name,
+ user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackStoppedNotificationType(item.MediaType),
@@ -187,7 +188,7 @@ namespace Emby.Server.Implementations.Activity
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
- user.Name,
+ user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackNotificationType(item.MediaType),
@@ -304,49 +305,37 @@ namespace Emby.Server.Implementations.Activity
}).ConfigureAwait(false);
}
- private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserPolicyUpdatedWithName"),
- e.Argument.Name),
- "UserPolicyUpdated",
- e.Argument.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+ private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserDeletedWithName"),
- e.Argument.Name),
+ e.Argument.Username),
"UserDeleted",
Guid.Empty))
.ConfigureAwait(false);
}
- private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+ private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPasswordChangedWithName"),
- e.Argument.Name),
+ e.Argument.Username),
"UserPasswordChanged",
e.Argument.Id))
.ConfigureAwait(false);
}
- private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+ private async void OnUserCreated(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserCreatedWithName"),
- e.Argument.Name),
+ e.Argument.Username),
"UserCreated",
e.Argument.Id))
.ConfigureAwait(false);
@@ -510,11 +499,10 @@ namespace Emby.Server.Implementations.Activity
_subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
- _userManager.UserCreated -= OnUserCreated;
- _userManager.UserPasswordChanged -= OnUserPasswordChanged;
- _userManager.UserDeleted -= OnUserDeleted;
- _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
- _userManager.UserLockedOut -= OnUserLockedOut;
+ _userManager.OnUserCreated -= OnUserCreated;
+ _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
+ _userManager.OnUserDeleted -= OnUserDeleted;
+ _userManager.OnUserLockedOut -= OnUserLockedOut;
}
/// <summary>
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 184c6f399..25ee7e9ec 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -97,7 +97,6 @@ using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles;
-using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@@ -562,11 +561,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
- serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>();
-
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
- serviceCollection.AddSingleton<IUserManager, UserManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
// TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
@@ -659,15 +655,11 @@ namespace Emby.Server.Implementations
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
- ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
SetStaticProperties();
- var userManager = (UserManager)Resolve<IUserManager>();
- userManager.Initialize();
-
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
- ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager);
+ ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
FindParts();
}
@@ -750,7 +742,6 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
- User.UserManager = Resolve<IUserManager>();
BaseItem.FileSystem = _fileSystemManager;
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
@@ -1045,9 +1036,6 @@ namespace Emby.Server.Implementations
// Include composable parts in the Api assembly
yield return typeof(ApiEntryPoint).Assembly;
- // Include composable parts in the Dashboard assembly
- yield return typeof(DashboardService).Assembly;
-
// Include composable parts in the Model assembly
yield return typeof(SystemInfo).Assembly;
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index f9eea66b4..519c5bbef 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
@@ -13,8 +14,6 @@ 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.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels;
@@ -24,6 +23,11 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.Channels
{
@@ -791,7 +795,8 @@ namespace Emby.Server.Implementations.Channels
return result;
}
- private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
+ private async Task<ChannelItemResult> GetChannelItems(
+ IChannel channel,
User user,
string externalFolderId,
ChannelItemSortField? sortField,
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index cba3975cf..8fb9520d6 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
index d474f1c6b..63d0321b7 100644
--- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 6ee6230fc..b99b74ef8 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
deleted file mode 100644
index 0c3f26974..000000000
--- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs
+++ /dev/null
@@ -1,240 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.Json;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Persistence;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
- /// <summary>
- /// Class SQLiteUserRepository
- /// </summary>
- public class SqliteUserRepository : BaseSqliteRepository, IUserRepository
- {
- private readonly JsonSerializerOptions _jsonOptions;
-
- public SqliteUserRepository(
- ILogger<SqliteUserRepository> logger,
- IServerApplicationPaths appPaths)
- : base(logger)
- {
- _jsonOptions = JsonDefaults.GetOptions();
-
- DbFilePath = Path.Combine(appPaths.DataPath, "users.db");
- }
-
- /// <summary>
- /// Gets the name of the repository
- /// </summary>
- /// <value>The name.</value>
- public string Name => "SQLite";
-
- /// <summary>
- /// Opens the connection to the database.
- /// </summary>
- public void Initialize()
- {
- using (var connection = GetConnection())
- {
- var localUsersTableExists = TableExists(connection, "LocalUsersv2");
-
- connection.RunQueries(new[] {
- "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)",
- "drop index if exists idx_users"
- });
-
- if (!localUsersTableExists && TableExists(connection, "Users"))
- {
- TryMigrateToLocalUsersTable(connection);
- }
-
- RemoveEmptyPasswordHashes(connection);
- }
- }
-
- private void TryMigrateToLocalUsersTable(ManagedConnection connection)
- {
- try
- {
- connection.RunQueries(new[]
- {
- "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users"
- });
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error migrating users database");
- }
- }
-
- private void RemoveEmptyPasswordHashes(ManagedConnection connection)
- {
- foreach (var user in RetrieveAllUsers(connection))
- {
- // If the user password is the sha1 hash of the empty string, remove it
- if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
- && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
- {
- continue;
- }
-
- user.Password = null;
- var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
- {
- statement.TryBind("@InternalId", user.InternalId);
- statement.TryBind("@data", serialized);
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
-
- /// <summary>
- /// Save a user in the repo
- /// </summary>
- public void CreateUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
- {
- statement.TryBind("@guid", user.Id.ToByteArray());
- statement.TryBind("@data", serialized);
-
- statement.MoveNext();
- }
-
- var createdUser = GetUser(user.Id, connection);
-
- if (createdUser == null)
- {
- throw new ApplicationException("created user should never be null");
- }
-
- user.InternalId = createdUser.InternalId;
-
- }, TransactionMode);
- }
- }
-
- public void UpdateUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
- {
- statement.TryBind("@InternalId", user.InternalId);
- statement.TryBind("@data", serialized);
- statement.MoveNext();
- }
-
- }, TransactionMode);
- }
- }
-
- private User GetUser(Guid guid, ManagedConnection connection)
- {
- using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
- {
- statement.TryBind("@guid", guid);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetUser(row);
- }
- }
-
- return null;
- }
-
- private User GetUser(IReadOnlyList<IResultSetValue> row)
- {
- var id = row[0].ToInt64();
- var guid = row[1].ReadGuidFromBlob();
-
- var user = JsonSerializer.Deserialize<User>(row[2].ToBlob(), _jsonOptions);
- user.InternalId = id;
- user.Id = guid;
- return user;
- }
-
- /// <summary>
- /// Retrieve all users from the database
- /// </summary>
- /// <returns>IEnumerable{User}.</returns>
- public List<User> RetrieveAllUsers()
- {
- using (var connection = GetConnection(true))
- {
- return new List<User>(RetrieveAllUsers(connection));
- }
- }
-
- /// <summary>
- /// Retrieve all users from the database
- /// </summary>
- /// <returns>IEnumerable{User}.</returns>
- private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection)
- {
- foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
- {
- yield return GetUser(row);
- }
- }
-
- /// <summary>
- /// Deletes the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- public void DeleteUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
- {
- statement.TryBind("@id", user.InternalId);
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index e39e0aa78..f8896d69f 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -5,10 +5,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
@@ -16,7 +17,6 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
-using MediaBrowser.Model.Users;
namespace Emby.Server.Implementations.Devices
{
@@ -27,11 +27,10 @@ namespace Emby.Server.Implementations.Devices
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;
- private readonly object _capabilitiesSyncLock = new object();
-
public DeviceManager(
IAuthenticationRepository authRepo,
IJsonSerializer json,
@@ -175,7 +174,12 @@ namespace Emby.Server.Implementations.Devices
throw new ArgumentNullException(nameof(deviceId));
}
- if (!CanAccessDevice(user.Policy, deviceId))
+ if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
+ {
+ return true;
+ }
+
+ if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
{
var capabilities = GetCapabilities(deviceId);
@@ -187,20 +191,5 @@ namespace Emby.Server.Implementations.Devices
return true;
}
-
- private static bool CanAccessDevice(UserPolicy policy, string id)
- {
- if (policy.EnableAllDevices)
- {
- return true;
- }
-
- if (policy.IsAdministrator)
- {
- return true;
- }
-
- return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index f9841082c..7e0cfe424 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -6,14 +6,14 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
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.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -24,6 +24,14 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Person = MediaBrowser.Controller.Entities.Person;
+using Photo = MediaBrowser.Controller.Entities.Photo;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.Dto
{
@@ -384,7 +392,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ChildCount))
{
- dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user);
+ dto.ChildCount ??= GetChildCount(folder, user);
}
}
@@ -414,7 +422,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.BasicSyncInfo))
{
- var userCanSync = user != null && user.Policy.EnableContentDownloading;
+ var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading);
if (userCanSync && item.SupportsExternalTransfer)
{
dto.SupportsSync = true;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index e71e437ac..5272e2692 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -13,7 +13,6 @@
<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" />
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index b9992ae73..02ae55071 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index b64439802..632735910 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints
private async Task SendMessage(string name, TimerEventInfo info)
{
- var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList();
+ var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
try
{
diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
deleted file mode 100644
index 54f4b67e6..000000000
--- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Tasks;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Class RefreshUsersMetadata.
- /// </summary>
- public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
- {
- /// <summary>
- /// The user manager.
- /// </summary>
- private readonly IUserManager _userManager;
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
- /// </summary>
- public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
- {
- _userManager = userManager;
- _fileSystem = fileSystem;
- }
-
- /// <inheritdoc />
- public string Name => "Refresh Users";
-
- /// <inheritdoc />
- public string Key => "RefreshUsers";
-
- /// <inheritdoc />
- public string Description => "Refresh user infos";
-
- /// <inheritdoc />
- public string Category => "Library";
-
- /// <inheritdoc />
- public bool IsHidden => true;
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public bool IsLogged => true;
-
- /// <inheritdoc />
- public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
- {
- foreach (var user in _userManager.Users)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
- }
- }
-
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
- {
- return new[]
- {
- new TaskTriggerInfo
- {
- IntervalTicks = TimeSpan.FromDays(1).Ticks,
- Type = TaskTriggerInfo.TriggerInterval
- }
- };
- }
- }
-}
diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
index 11ba6f748..083fe4237 100644
--- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
@@ -68,10 +68,8 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc />
public Task RunAsync()
{
- _userManager.UserDeleted += OnUserDeleted;
- _userManager.UserUpdated += OnUserUpdated;
- _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
- _userManager.UserConfigurationUpdated += OnUserConfigurationUpdated;
+ _userManager.OnUserDeleted += OnUserDeleted;
+ _userManager.OnUserUpdated += OnUserUpdated;
_appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
@@ -153,20 +151,6 @@ namespace Emby.Server.Implementations.EntryPoints
await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
- private async void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
- {
- var dto = _userManager.GetUserDto(e.Argument);
-
- await SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto).ConfigureAwait(false);
- }
-
- private async void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e)
- {
- var dto = _userManager.GetUserDto(e.Argument);
-
- await SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto).ConfigureAwait(false);
- }
-
private async Task SendMessageToAdminSessions<T>(string name, T data)
{
try
@@ -210,10 +194,8 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (dispose)
{
- _userManager.UserDeleted -= OnUserDeleted;
- _userManager.UserUpdated -= OnUserUpdated;
- _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
- _userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated;
+ _userManager.OnUserDeleted -= OnUserDeleted;
+ _userManager.OnUserUpdated -= OnUserUpdated;
_installationManager.PluginUninstalled -= OnPluginUninstalled;
_installationManager.PackageInstalling -= OnPackageInstalling;
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
index 85c3db9b2..3ab5dbc16 100644
--- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
+++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.Text;
using MediaBrowser.Controller.Net;
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 07e199d32..6a2d8fdbb 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -3,10 +3,11 @@
using System;
using System.Linq;
using Emby.Server.Implementations.SocketSharp;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
@@ -38,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_networkManager = networkManager;
}
- public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
+ public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{
- ValidateUser(request, authAttribtues);
+ ValidateUser(request, authAttributes);
}
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@@ -50,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
return user;
}
- private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
+ public AuthorizationInfo Authenticate(HttpRequest request)
+ {
+ var auth = _authorizationContext.GetAuthorizationInfo(request);
+ if (auth?.User == null)
+ {
+ return null;
+ }
+
+ if (auth.User.HasPermission(PermissionKind.IsDisabled))
+ {
+ throw new SecurityException("User account has been disabled.");
+ }
+
+ return auth;
+ }
+
+ private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{
// This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
- if (!IsExemptFromAuthenticationToken(authAttribtues, request))
+ if (!IsExemptFromAuthenticationToken(authAttributes, request))
{
ValidateSecurityToken(request, auth.Token);
}
- if (authAttribtues.AllowLocalOnly && !request.IsLocal)
+ if (authAttributes.AllowLocalOnly && !request.IsLocal)
{
throw new SecurityException("Operation not found.");
}
@@ -74,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user != null)
{
- ValidateUserAccess(user, request, authAttribtues, auth);
+ ValidateUserAccess(user, request, authAttributes);
}
var info = GetTokenInfo(request);
- if (!IsExemptFromRoles(auth, authAttribtues, request, info))
+ if (!IsExemptFromRoles(auth, authAttributes, request, info))
{
- var roles = authAttribtues.GetRoles();
+ var roles = authAttributes.GetRoles();
ValidateRoles(roles, user);
}
@@ -90,7 +107,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
!string.IsNullOrEmpty(auth.Client) &&
!string.IsNullOrEmpty(auth.Device))
{
- _sessionManager.LogSessionActivity(auth.Client,
+ _sessionManager.LogSessionActivity(
+ auth.Client,
auth.Version,
auth.DeviceId,
auth.Device,
@@ -104,21 +122,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess(
User user,
IRequest request,
- IAuthenticationAttributes authAttribtues,
- AuthorizationInfo auth)
+ IAuthenticationAttributes authAttributes)
{
- if (user.Policy.IsDisabled)
+ if (user.HasPermission(PermissionKind.IsDisabled))
{
throw new SecurityException("User account has been disabled.");
}
- if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
+ if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
{
throw new SecurityException("User account has been disabled.");
}
- if (!user.Policy.IsAdministrator
- && !authAttribtues.EscapeParentalControl
+ if (!user.HasPermission(PermissionKind.IsAdministrator)
+ && !authAttributes.EscapeParentalControl
&& !user.IsParentalScheduleAllowed())
{
request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
@@ -186,7 +203,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
{
- if (user == null || !user.Policy.IsAdministrator)
+ if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
{
throw new SecurityException("User does not have admin access.");
}
@@ -194,7 +211,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
{
- if (user == null || !user.Policy.EnableContentDeletion)
+ if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
{
throw new SecurityException("User does not have delete access.");
}
@@ -202,7 +219,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
{
- if (user == null || !user.Policy.EnableContentDownloading)
+ if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
{
throw new SecurityException("User does not have download access.");
}
@@ -228,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
throw new AuthenticationException("Access token is invalid or expired.");
}
-
- //if (!string.IsNullOrEmpty(info.UserId))
- //{
- // var user = _userManager.GetUserById(info.UserId);
-
- // if (user == null || user.Configuration.IsDisabled)
- // {
- // throw new SecurityException("User account has been disabled.");
- // }
- //}
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 129faeaab..fb93fae3e 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
@@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(requestContext);
}
+ public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
+ {
+ var auth = GetAuthorizationDictionary(requestContext);
+ var (authInfo, _) =
+ GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+ return authInfo;
+ }
+
/// <summary>
/// Gets the authorization.
/// </summary>
@@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(IRequest httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
+ var (authInfo, originalAuthInfo) =
+ GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
+ if (originalAuthInfo != null)
+ {
+ httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+ }
+
+ httpReq.Items["AuthorizationInfo"] = authInfo;
+ return authInfo;
+ }
+
+ private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+ in Dictionary<string, string> auth,
+ in IHeaderDictionary headers,
+ in IQueryCollection queryString)
+ {
string deviceId = null;
string device = null;
string client = null;
@@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token))
{
- token = httpReq.Headers["X-Emby-Token"];
+ token = headers["X-Emby-Token"];
}
if (string.IsNullOrEmpty(token))
{
- token = httpReq.Headers["X-MediaBrowser-Token"];
+ token = headers["X-MediaBrowser-Token"];
}
+
+ if (string.IsNullOrEmpty(token))
+ {
+ token = queryString["ApiKey"];
+ }
+
+ // TODO deprecate this query parameter.
if (string.IsNullOrEmpty(token))
{
- token = httpReq.QueryString["api_key"];
+ token = queryString["api_key"];
}
- var info = new AuthorizationInfo
+ var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
@@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
Token = token
};
+ AuthenticationInfo originalAuthenticationInfo = null;
if (!string.IsNullOrWhiteSpace(token))
{
var result = _authRepo.Get(new AuthenticationInfoQuery
@@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
AccessToken = token
});
- var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
+ originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
- if (tokenInfo != null)
+ if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
- if (string.IsNullOrWhiteSpace(info.Client))
+ if (string.IsNullOrWhiteSpace(authInfo.Client))
{
- info.Client = tokenInfo.AppName;
+ authInfo.Client = originalAuthenticationInfo.AppName;
}
- if (string.IsNullOrWhiteSpace(info.DeviceId))
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
- info.DeviceId = tokenInfo.DeviceId;
+ authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+ var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
- if (string.IsNullOrWhiteSpace(info.Device))
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
{
- info.Device = tokenInfo.DeviceName;
+ authInfo.Device = originalAuthenticationInfo.DeviceName;
}
-
- else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- tokenInfo.DeviceName = info.Device;
+ originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
- if (string.IsNullOrWhiteSpace(info.Version))
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
{
- info.Version = tokenInfo.AppVersion;
+ authInfo.Version = originalAuthenticationInfo.AppVersion;
}
- else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- tokenInfo.AppVersion = info.Version;
+ originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
- if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
+ if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{
- tokenInfo.DateLastActivity = DateTime.UtcNow;
+ originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
- if (!tokenInfo.UserId.Equals(Guid.Empty))
+ if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
- info.User = _userManager.GetUserById(tokenInfo.UserId);
+ authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
- if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
+ if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
- tokenInfo.UserName = info.User.Name;
+ originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
}
if (updateToken)
{
- _authRepo.Update(tokenInfo);
+ _authRepo.Update(originalAuthenticationInfo);
}
}
- httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
}
- httpReq.Items["AuthorizationInfo"] = info;
-
- return info;
+ return (authInfo, originalAuthenticationInfo);
}
/// <summary>
@@ -187,6 +216,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
}
/// <summary>
+ /// Gets the auth.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
+ {
+ var auth = httpReq.Headers["X-Emby-Authorization"];
+
+ if (string.IsNullOrEmpty(auth))
+ {
+ auth = httpReq.Headers[HeaderNames.Authorization];
+ }
+
+ return GetAuthorization(auth);
+ }
+
+ /// <summary>
/// Gets the authorization.
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
@@ -236,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/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 166952c64..03fcfa53d 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index c64d57339..316cd84cf 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -234,10 +234,12 @@ namespace Emby.Server.Implementations.HttpServer
private Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
- return SendAsync(new WebSocketMessage<string>
- {
- MessageType = "KeepAlive"
- }, CancellationToken.None);
+ return SendAsync(
+ new WebSocketMessage<string>
+ {
+ MessageId = Guid.NewGuid(),
+ MessageType = "KeepAlive"
+ }, CancellationToken.None);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a57219143..7d74ced6c 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -17,6 +17,8 @@ using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -25,7 +27,6 @@ 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.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@@ -46,6 +47,9 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
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;
@@ -1539,7 +1543,8 @@ namespace Emby.Server.Implementations.Library
}
// Handle grouping
- if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0)
+ if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
+ && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{
return GetUserRootFolder()
.GetChildren(user, true)
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 00826c7cb..02add6600 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -7,6 +7,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@@ -14,7 +16,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -190,10 +191,7 @@ namespace Emby.Server.Implementations.Library
{
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
- if (!user.Policy.EnableAudioPlaybackTranscoding)
- {
- source.SupportsTranscoding = false;
- }
+ source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
}
}
@@ -352,7 +350,9 @@ namespace Emby.Server.Implementations.Library
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
- if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+ if (userData.SubtitleStreamIndex.HasValue
+ && user.RememberSubtitleSelections
+ && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
{
var index = userData.SubtitleStreamIndex.Value;
// Make sure the saved index is still valid
@@ -363,26 +363,27 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
- ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference);
+
+ var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
+ ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
? null
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
- source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
+ source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(
+ source.MediaStreams,
preferredSubs,
- user.Configuration.SubtitleMode,
+ user.SubtitleMode,
audioLangage);
- MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
- user.Configuration.SubtitleMode, audioLangage);
+ MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
}
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
- if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection)
+ if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
{
var index = userData.AudioStreamIndex.Value;
// Make sure the saved index is still valid
@@ -393,11 +394,11 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
+ var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
? Array.Empty<string>()
- : NormalizeLanguage(user.Configuration.AudioLanguagePreference);
+ : NormalizeLanguage(user.AudioLanguagePreference);
- source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
+ source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
@@ -534,7 +535,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.RunTimeTicks = null;
}
- var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+ var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1)
{
@@ -545,7 +546,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
}
- var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+ var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null)
{
if (!videoStream.BitRate.HasValue)
@@ -556,17 +557,14 @@ namespace Emby.Server.Implementations.Library
{
videoStream.BitRate = 30000000;
}
-
else if (width >= 1900)
{
videoStream.BitRate = 20000000;
}
-
else if (width >= 1200)
{
videoStream.BitRate = 8000000;
}
-
else if (width >= 700)
{
videoStream.BitRate = 2000000;
@@ -670,13 +668,14 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000;
}
- mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
+ mediaInfo = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
{
MediaSource = mediaSource,
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false
-
- }, cancellationToken).ConfigureAwait(false);
+ },
+ cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null)
{
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index e27145a1d..ca904c4ec 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using MediaBrowser.Model.Configuration;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 1ec578371..0bdc59914 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -10,6 +11,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace Emby.Server.Implementations.Library
{
@@ -75,7 +77,6 @@ namespace Emby.Server.Implementations.Library
{
return Guid.Empty;
}
-
}).Where(i => !i.Equals(Guid.Empty)).ToArray();
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
@@ -105,32 +106,27 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
}
- var playlist = item as Playlist;
- if (playlist != null)
+ if (item is Playlist playlist)
{
return GetInstantMixFromPlaylist(playlist, user, dtoOptions);
}
- var album = item as MusicAlbum;
- if (album != null)
+ if (item is MusicAlbum album)
{
return GetInstantMixFromAlbum(album, user, dtoOptions);
}
- var artist = item as MusicArtist;
- if (artist != null)
+ if (item is MusicArtist artist)
{
return GetInstantMixFromArtist(artist, user, dtoOptions);
}
- var song = item as Audio;
- if (song != null)
+ if (item is Audio song)
{
return GetInstantMixFromSong(song, user, dtoOptions);
}
- var folder = item as Folder;
- if (folder != null)
+ if (item is Folder folder)
{
return GetInstantMixFromFolder(folder, user, dtoOptions);
}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index bde77ee8e..f37cb3e9d 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -12,6 +13,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index a463d37f0..9e17fa672 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -13,6 +14,7 @@ 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
{
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
deleted file mode 100644
index 531e00666..000000000
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ /dev/null
@@ -1,1107 +0,0 @@
-#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.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Cryptography;
-using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Library
-{
- /// <summary>
- /// Class UserManager.
- /// </summary>
- public class UserManager : IUserManager
- {
- private readonly object _policySyncLock = new object();
- private readonly object _configSyncLock = new object();
-
- private readonly ILogger<UserManager> _logger;
- private readonly IUserRepository _userRepository;
- private readonly IXmlSerializer _xmlSerializer;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly INetworkManager _networkManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly Lazy<IDtoService> _dtoServiceFactory;
- private readonly IServerApplicationHost _appHost;
- private readonly IFileSystem _fileSystem;
- private readonly ICryptoProvider _cryptoProvider;
-
- private ConcurrentDictionary<Guid, User> _users;
-
- private IAuthenticationProvider[] _authenticationProviders;
- private DefaultAuthenticationProvider _defaultAuthenticationProvider;
-
- private InvalidAuthProvider _invalidAuthProvider;
-
- private IPasswordResetProvider[] _passwordResetProviders;
- private DefaultPasswordResetProvider _defaultPasswordResetProvider;
-
- private IDtoService DtoService => _dtoServiceFactory.Value;
-
- public UserManager(
- ILogger<UserManager> logger,
- IUserRepository userRepository,
- IXmlSerializer xmlSerializer,
- INetworkManager networkManager,
- IImageProcessor imageProcessor,
- Lazy<IDtoService> dtoServiceFactory,
- IServerApplicationHost appHost,
- IJsonSerializer jsonSerializer,
- IFileSystem fileSystem,
- ICryptoProvider cryptoProvider)
- {
- _logger = logger;
- _userRepository = userRepository;
- _xmlSerializer = xmlSerializer;
- _networkManager = networkManager;
- _imageProcessor = imageProcessor;
- _dtoServiceFactory = dtoServiceFactory;
- _appHost = appHost;
- _jsonSerializer = jsonSerializer;
- _fileSystem = fileSystem;
- _cryptoProvider = cryptoProvider;
- _users = null;
- }
-
- public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
-
- /// <summary>
- /// Occurs when [user updated].
- /// </summary>
- public event EventHandler<GenericEventArgs<User>> UserUpdated;
-
- public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
-
- public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
-
- public event EventHandler<GenericEventArgs<User>> UserLockedOut;
-
- public event EventHandler<GenericEventArgs<User>> UserCreated;
-
- /// <summary>
- /// Occurs when [user deleted].
- /// </summary>
- public event EventHandler<GenericEventArgs<User>> UserDeleted;
-
- /// <inheritdoc />
- public IEnumerable<User> Users => _users.Values;
-
- /// <inheritdoc />
- public IEnumerable<Guid> UsersIds => _users.Keys;
-
- /// <summary>
- /// Called when [user updated].
- /// </summary>
- /// <param name="user">The user.</param>
- private void OnUserUpdated(User user)
- {
- UserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
- }
-
- /// <summary>
- /// Called when [user deleted].
- /// </summary>
- /// <param name="user">The user.</param>
- private void OnUserDeleted(User user)
- {
- UserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
- }
-
- public NameIdPair[] GetAuthenticationProviders()
- {
- return _authenticationProviders
- .Where(i => i.IsEnabled)
- .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
- .ThenBy(i => i.Name)
- .Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = GetAuthenticationProviderId(i)
- })
- .ToArray();
- }
-
- public NameIdPair[] GetPasswordResetProviders()
- {
- return _passwordResetProviders
- .Where(i => i.IsEnabled)
- .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
- .ThenBy(i => i.Name)
- .Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = GetPasswordResetProviderId(i)
- })
- .ToArray();
- }
-
- public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
- {
- _authenticationProviders = authenticationProviders.ToArray();
-
- _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
-
- _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
-
- _passwordResetProviders = passwordResetProviders.ToArray();
-
- _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
- }
-
- /// <inheritdoc />
- public User GetUserById(Guid id)
- {
- if (id == Guid.Empty)
- {
- throw new ArgumentException("Guid can't be empty", nameof(id));
- }
-
- _users.TryGetValue(id, out User user);
- return user;
- }
-
- public User GetUserByName(string name)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("Invalid username", nameof(name));
- }
-
- return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase));
- }
-
- public void Initialize()
- {
- LoadUsers();
-
- var users = Users;
-
- // If there are no local users with admin rights, make them all admins
- if (!users.Any(i => i.Policy.IsAdministrator))
- {
- foreach (var user in users)
- {
- user.Policy.IsAdministrator = true;
- UpdateUserPolicy(user, user.Policy, false);
- }
- }
- }
-
- public static bool IsValidUsername(string username)
- {
- // 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(username, @"^[\w\-'._@]*$");
- }
-
- private static bool IsValidUsernameCharacter(char i)
- => IsValidUsername(i.ToString(CultureInfo.InvariantCulture));
-
- public string MakeValidUsername(string username)
- {
- if (IsValidUsername(username))
- {
- return username;
- }
-
- // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
- var builder = new StringBuilder();
-
- foreach (var c in username)
- {
- if (IsValidUsernameCharacter(c))
- {
- builder.Append(c);
- }
- }
-
- return builder.ToString();
- }
-
- public async Task<User> AuthenticateUser(
- string username,
- string password,
- string hashedPassword,
- string remoteEndPoint,
- bool isUserSession)
- {
- if (string.IsNullOrWhiteSpace(username))
- {
- _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
- throw new ArgumentNullException(nameof(username));
- }
-
- var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
-
- var success = false;
- IAuthenticationProvider authenticationProvider = null;
-
- if (user != null)
- {
- var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
- success = authResult.success;
- }
- else
- {
- // user is null
- var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
- string updatedUsername = authResult.username;
- success = authResult.success;
-
- if (success
- && authenticationProvider != null
- && !(authenticationProvider is DefaultAuthenticationProvider))
- {
- // Trust the username returned by the authentication provider
- username = updatedUsername;
-
- // Search the database for the user again
- // the authentication provider might have created it
- user = Users
- .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
-
- if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
- {
- var policy = hasNewUserPolicy.GetNewUserPolicy();
- UpdateUserPolicy(user, policy, true);
- }
- }
- }
-
- if (success && user != null && authenticationProvider != null)
- {
- var providerId = GetAuthenticationProviderId(authenticationProvider);
-
- if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
- {
- user.Policy.AuthenticationProviderId = providerId;
- UpdateUserPolicy(user, user.Policy, true);
- }
- }
-
- if (user == null)
- {
- _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint);
- throw new AuthenticationException("Invalid username or password entered.");
- }
-
- if (user.Policy.IsDisabled)
- {
- _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint);
- throw new SecurityException($"The {user.Name} account is currently disabled. Please consult with your administrator.");
- }
-
- if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
- {
- _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint);
- throw new SecurityException("Forbidden.");
- }
-
- if (!user.IsParentalScheduleAllowed())
- {
- _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint);
- throw new SecurityException("User is not allowed access at this time.");
- }
-
- // Update LastActivityDate and LastLoginDate, then save
- if (success)
- {
- if (isUserSession)
- {
- user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
- UpdateUser(user);
- }
-
- ResetInvalidLoginAttemptCount(user);
- _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name);
- }
- else
- {
- IncrementInvalidLoginAttemptCount(user);
- _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint);
- }
-
- return success ? user : null;
- }
-
-#nullable enable
-
- private static string GetAuthenticationProviderId(IAuthenticationProvider provider)
- {
- return provider.GetType().FullName;
- }
-
- private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
- {
- return provider.GetType().FullName;
- }
-
- private IAuthenticationProvider GetAuthenticationProvider(User user)
- {
- return GetAuthenticationProviders(user)[0];
- }
-
- private IPasswordResetProvider GetPasswordResetProvider(User user)
- {
- return GetPasswordResetProviders(user)[0];
- }
-
- private IAuthenticationProvider[] GetAuthenticationProviders(User? user)
- {
- var authenticationProviderId = user?.Policy.AuthenticationProviderId;
-
- var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray();
-
- if (!string.IsNullOrEmpty(authenticationProviderId))
- {
- providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
- }
-
- if (providers.Length == 0)
- {
- // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
- _logger.LogWarning("User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user?.Name, user?.Policy.AuthenticationProviderId);
- providers = new IAuthenticationProvider[] { _invalidAuthProvider };
- }
-
- return providers;
- }
-
- private IPasswordResetProvider[] GetPasswordResetProviders(User? user)
- {
- var passwordResetProviderId = user?.Policy.PasswordResetProviderId;
-
- var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
-
- if (!string.IsNullOrEmpty(passwordResetProviderId))
- {
- providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
- }
-
- if (providers.Length == 0)
- {
- providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider };
- }
-
- return providers;
- }
-
- private async Task<(string username, bool success)> AuthenticateWithProvider(
- IAuthenticationProvider provider,
- string username,
- string password,
- User? resolvedUser)
- {
- try
- {
- var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
- ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
- : await provider.Authenticate(username, password).ConfigureAwait(false);
-
- if (authenticationResult.Username != username)
- {
- _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
- username = authenticationResult.Username;
- }
-
- return (username, true);
- }
- catch (AuthenticationException ex)
- {
- _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
-
- return (username, false);
- }
- }
-
- private async Task<(IAuthenticationProvider? authenticationProvider, string username, bool success)> AuthenticateLocalUser(
- string username,
- string password,
- string hashedPassword,
- User? user,
- string remoteEndPoint)
- {
- bool success = false;
- IAuthenticationProvider? authenticationProvider = null;
-
- foreach (var provider in GetAuthenticationProviders(user))
- {
- var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
- var updatedUsername = providerAuthResult.username;
- success = providerAuthResult.success;
-
- if (success)
- {
- authenticationProvider = provider;
- username = updatedUsername;
- break;
- }
- }
-
- if (!success
- && _networkManager.IsInLocalNetwork(remoteEndPoint)
- && user?.Configuration.EnableLocalPassword == true
- && !string.IsNullOrEmpty(user.EasyPassword))
- {
- // Check easy password
- var passwordHash = PasswordHash.Parse(user.EasyPassword);
- var hash = _cryptoProvider.ComputeHash(
- passwordHash.Id,
- Encoding.UTF8.GetBytes(password),
- passwordHash.Salt.ToArray());
- success = passwordHash.Hash.SequenceEqual(hash);
- }
-
- return (authenticationProvider, username, success);
- }
-
- private void ResetInvalidLoginAttemptCount(User user)
- {
- user.Policy.InvalidLoginAttemptCount = 0;
- UpdateUserPolicy(user, user.Policy, false);
- }
-
- private void IncrementInvalidLoginAttemptCount(User user)
- {
- int invalidLogins = ++user.Policy.InvalidLoginAttemptCount;
- int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout;
- if (maxInvalidLogins > 0
- && invalidLogins >= maxInvalidLogins)
- {
- user.Policy.IsDisabled = true;
- UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
- _logger.LogWarning(
- "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.",
- user.Name,
- invalidLogins);
- }
-
- UpdateUserPolicy(user, user.Policy, false);
- }
-
- /// <summary>
- /// Loads the users from the repository.
- /// </summary>
- private void LoadUsers()
- {
- var users = _userRepository.RetrieveAllUsers();
-
- // There always has to be at least one user.
- if (users.Count != 0)
- {
- _users = new ConcurrentDictionary<Guid, User>(
- users.Select(x => new KeyValuePair<Guid, User>(x.Id, x)));
- return;
- }
-
- var defaultName = Environment.UserName;
- if (string.IsNullOrWhiteSpace(defaultName))
- {
- defaultName = "MyJellyfinUser";
- }
-
- _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
-
- var name = MakeValidUsername(defaultName);
-
- var user = InstantiateNewUser(name);
-
- user.DateLastSaved = DateTime.UtcNow;
-
- _userRepository.CreateUser(user);
-
- user.Policy.IsAdministrator = true;
- user.Policy.EnableContentDeletion = true;
- user.Policy.EnableRemoteControlOfOtherUsers = true;
- UpdateUserPolicy(user, user.Policy, false);
-
- _users = new ConcurrentDictionary<Guid, User>();
- _users[user.Id] = user;
- }
-
-#nullable restore
-
- public UserDto GetUserDto(User user, string remoteEndPoint = null)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user);
- bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
-
- bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
- hasConfiguredEasyPassword :
- hasConfiguredPassword;
-
- UserDto dto = new UserDto
- {
- Id = user.Id,
- Name = user.Name,
- HasPassword = hasPassword,
- HasConfiguredPassword = hasConfiguredPassword,
- HasConfiguredEasyPassword = hasConfiguredEasyPassword,
- LastActivityDate = user.LastActivityDate,
- LastLoginDate = user.LastLoginDate,
- Configuration = user.Configuration,
- ServerId = _appHost.SystemId,
- Policy = user.Policy
- };
-
- if (!hasPassword && _users.Count == 1)
- {
- dto.EnableAutoLogin = true;
- }
-
- ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
-
- if (image != null)
- {
- dto.PrimaryImageTag = GetImageCacheTag(user, image);
-
- try
- {
- DtoService.AttachPrimaryImageAspectRatio(dto, user);
- }
- catch (Exception ex)
- {
- // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
- _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {User}", user.Name);
- }
- }
-
- return dto;
- }
-
- public UserDto GetOfflineUserDto(User user)
- {
- var dto = GetUserDto(user);
-
- dto.ServerName = _appHost.FriendlyName;
-
- return dto;
- }
-
- private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
- {
- try
- {
- return _imageProcessor.GetImageCacheTag(item, image);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting {ImageType} image info for {ImagePath}", image.Type, image.Path);
- return null;
- }
- }
-
- /// <summary>
- /// Refreshes metadata for each user
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task RefreshUsersMetadata(CancellationToken cancellationToken)
- {
- foreach (var user in Users)
- {
- await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Renames the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <param name="newName">The new name.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
- public async Task RenameUser(User user, string newName)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- if (string.IsNullOrWhiteSpace(newName))
- {
- throw new ArgumentException("Invalid username", nameof(newName));
- }
-
- if (user.Name.Equals(newName, StringComparison.Ordinal))
- {
- throw new ArgumentException("The new and old names must be different.");
- }
-
- if (Users.Any(
- u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
- {
- throw new ArgumentException(string.Format(
- CultureInfo.InvariantCulture,
- "A user with the name '{0}' already exists.",
- newName));
- }
-
- await user.Rename(newName).ConfigureAwait(false);
-
- OnUserUpdated(user);
- }
-
- /// <summary>
- /// Updates the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
- public void UpdateUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- if (user.Id == Guid.Empty)
- {
- throw new ArgumentException("Id can't be empty.", nameof(user));
- }
-
- if (!_users.ContainsKey(user.Id))
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "A user '{0}' with Id {1} does not exist.",
- user.Name,
- user.Id),
- nameof(user));
- }
-
- user.DateModified = DateTime.UtcNow;
- user.DateLastSaved = DateTime.UtcNow;
-
- _userRepository.UpdateUser(user);
-
- OnUserUpdated(user);
- }
-
- /// <summary>
- /// Creates the user.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <returns>User.</returns>
- /// <exception cref="ArgumentNullException">name</exception>
- /// <exception cref="ArgumentException"></exception>
- public User CreateUser(string name)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- if (!IsValidUsername(name))
- {
- throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
- }
-
- if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
- {
- throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
- }
-
- var user = InstantiateNewUser(name);
-
- _users[user.Id] = user;
-
- user.DateLastSaved = DateTime.UtcNow;
-
- _userRepository.CreateUser(user);
-
- EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User>(user), _logger);
-
- return user;
- }
-
- /// <inheritdoc />
- /// <exception cref="ArgumentNullException">The <c>user</c> is <c>null</c>.</exception>
- /// <exception cref="ArgumentException">The <c>user</c> doesn't exist, or is the last administrator.</exception>
- /// <exception cref="InvalidOperationException">The <c>user</c> can't be deleted; there are no other users.</exception>
- public void DeleteUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- if (!_users.ContainsKey(user.Id))
- {
- throw new ArgumentException(string.Format(
- CultureInfo.InvariantCulture,
- "The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
- user.Name,
- user.Id));
- }
-
- if (_users.Count == 1)
- {
- throw new InvalidOperationException(string.Format(
- CultureInfo.InvariantCulture,
- "The user '{0}' cannot be deleted because there must be at least one user in the system.",
- user.Name));
- }
-
- if (user.Policy.IsAdministrator
- && Users.Count(i => i.Policy.IsAdministrator) == 1)
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
- user.Name),
- nameof(user));
- }
-
- var configPath = GetConfigurationFilePath(user);
-
- _userRepository.DeleteUser(user);
-
- // Delete user config dir
- lock (_configSyncLock)
- lock (_policySyncLock)
- {
- try
- {
- Directory.Delete(user.ConfigurationDirectoryPath, true);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting user config dir: {Path}", user.ConfigurationDirectoryPath);
- }
- }
-
- _users.TryRemove(user.Id, out _);
-
- OnUserDeleted(user);
- }
-
- /// <summary>
- /// Resets the password by clearing it.
- /// </summary>
- /// <returns>Task.</returns>
- public Task ResetPassword(User user)
- {
- return ChangePassword(user, string.Empty);
- }
-
- public void ResetEasyPassword(User user)
- {
- ChangeEasyPassword(user, string.Empty, null);
- }
-
- public async Task ChangePassword(User user, string newPassword)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
-
- UpdateUser(user);
-
- UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
- }
-
- public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash);
-
- UpdateUser(user);
-
- UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
- }
-
- /// <summary>
- /// Instantiates the new user.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <returns>User.</returns>
- private static User InstantiateNewUser(string name)
- {
- return new User
- {
- Name = name,
- Id = Guid.NewGuid(),
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow
- };
- }
-
- public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
- {
- var user = string.IsNullOrWhiteSpace(enteredUsername) ?
- null :
- GetUserByName(enteredUsername);
-
- var action = ForgotPasswordAction.InNetworkRequired;
-
- if (user != null && isInNetwork)
- {
- var passwordResetProvider = GetPasswordResetProvider(user);
- return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
- }
- else
- {
- return new ForgotPasswordResult
- {
- Action = action,
- PinFile = string.Empty
- };
- }
- }
-
- public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
- {
- foreach (var provider in _passwordResetProviders)
- {
- var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
- if (result.Success)
- {
- return result;
- }
- }
-
- return new PinRedeemResult
- {
- Success = false,
- UsersReset = Array.Empty<string>()
- };
- }
-
- public UserPolicy GetUserPolicy(User user)
- {
- var path = GetPolicyFilePath(user);
- if (!File.Exists(path))
- {
- return GetDefaultPolicy();
- }
-
- try
- {
- lock (_policySyncLock)
- {
- return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reading policy file: {Path}", path);
-
- return GetDefaultPolicy();
- }
- }
-
- private static UserPolicy GetDefaultPolicy()
- {
- return new UserPolicy
- {
- EnableContentDownloading = true,
- EnableSyncTranscoding = true
- };
- }
-
- public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy)
- {
- var user = GetUserById(userId);
- UpdateUserPolicy(user, userPolicy, true);
- }
-
- private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent)
- {
- // The xml serializer will output differently if the type is not exact
- if (userPolicy.GetType() != typeof(UserPolicy))
- {
- var json = _jsonSerializer.SerializeToString(userPolicy);
- userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json);
- }
-
- var path = GetPolicyFilePath(user);
-
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- lock (_policySyncLock)
- {
- _xmlSerializer.SerializeToFile(userPolicy, path);
- user.Policy = userPolicy;
- }
-
- if (fireEvent)
- {
- UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User>(user));
- }
- }
-
- private static string GetPolicyFilePath(User user)
- {
- return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml");
- }
-
- private static string GetConfigurationFilePath(User user)
- {
- return Path.Combine(user.ConfigurationDirectoryPath, "config.xml");
- }
-
- public UserConfiguration GetUserConfiguration(User user)
- {
- var path = GetConfigurationFilePath(user);
-
- if (!File.Exists(path))
- {
- return new UserConfiguration();
- }
-
- try
- {
- lock (_configSyncLock)
- {
- return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reading policy file: {Path}", path);
-
- return new UserConfiguration();
- }
- }
-
- public void UpdateConfiguration(Guid userId, UserConfiguration config)
- {
- var user = GetUserById(userId);
- UpdateConfiguration(user, config);
- }
-
- public void UpdateConfiguration(User user, UserConfiguration config)
- {
- UpdateConfiguration(user, config, true);
- }
-
- private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent)
- {
- var path = GetConfigurationFilePath(user);
-
- // The xml serializer will output differently if the type is not exact
- if (config.GetType() != typeof(UserConfiguration))
- {
- var json = _jsonSerializer.SerializeToString(config);
- config = _jsonSerializer.DeserializeFromString<UserConfiguration>(json);
- }
-
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- lock (_configSyncLock)
- {
- _xmlSerializer.SerializeToFile(config, path);
- user.Configuration = config;
- }
-
- if (fireEvent)
- {
- UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User>(user));
- }
- }
- }
-
- public class DeviceAccessEntryPoint : IServerEntryPoint
- {
- private IUserManager _userManager;
- private IAuthenticationRepository _authRepo;
- private IDeviceManager _deviceManager;
- private ISessionManager _sessionManager;
-
- public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
- {
- _userManager = userManager;
- _authRepo = authRepo;
- _deviceManager = deviceManager;
- _sessionManager = sessionManager;
- }
-
- public Task RunAsync()
- {
- _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated;
-
- return Task.CompletedTask;
- }
-
- private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e)
- {
- var user = e.Argument;
- if (!user.Policy.EnableAllDevices)
- {
- UpdateDeviceAccess(user);
- }
- }
-
- private void UpdateDeviceAccess(User user)
- {
- var existing = _authRepo.Get(new AuthenticationInfoQuery
- {
- UserId = user.Id
-
- }).Items;
-
- foreach (var authInfo in existing)
- {
- if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
- {
- _sessionManager.Logout(authInfo);
- }
- }
- }
-
- public void Dispose()
- {
-
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 322819b05..f51657c63 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -17,6 +19,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{
@@ -125,12 +129,12 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden)
{
- list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
+ list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
- var orders = user.Configuration.OrderedViews.ToList();
+ var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
return list
.OrderBy(i =>
@@ -165,7 +169,13 @@ namespace Emby.Server.Implementations.Library
return GetUserSubViewWithName(name, parentId, type, sortName);
}
- private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews)
+ private Folder GetUserView(
+ List<ICollectionFolder> parents,
+ string viewType,
+ string localizationKey,
+ string sortName,
+ Jellyfin.Data.Entities.User user,
+ string[] presetViews)
{
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
{
@@ -270,7 +280,8 @@ namespace Emby.Server.Implementations.Library
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
+ .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToList();
}
@@ -331,12 +342,11 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
{
- typeof(Person).Name,
- typeof(Studio).Name,
- typeof(Year).Name,
- typeof(MusicGenre).Name,
- typeof(Genre).Name
-
+ nameof(Person),
+ nameof(Studio),
+ nameof(Year),
+ nameof(MusicGenre),
+ nameof(Genre)
} : Array.Empty<string>();
var query = new InternalItemsQuery(user)
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index a88e0f31f..e39092f97 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -7,6 +7,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
@@ -14,8 +16,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -31,6 +31,8 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
namespace Emby.Server.Implementations.LiveTv
{
@@ -696,7 +698,6 @@ namespace Emby.Server.Implementations.LiveTv
{
Path = info.ThumbImageUrl,
Type = ImageType.Thumb
-
}, 0);
}
}
@@ -709,7 +710,6 @@ namespace Emby.Server.Implementations.LiveTv
{
Path = info.LogoImageUrl,
Type = ImageType.Logo
-
}, 0);
}
}
@@ -722,7 +722,6 @@ namespace Emby.Server.Implementations.LiveTv
{
Path = info.BackdropImageUrl,
Type = ImageType.Backdrop
-
}, 0);
}
}
@@ -760,7 +759,8 @@ namespace Emby.Server.Implementations.LiveTv
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
- var list = new List<Tuple<BaseItemDto, string, string>>() {
+ var list = new List<Tuple<BaseItemDto, string, string>>
+ {
new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, program.ExternalSeriesId)
};
@@ -2167,20 +2167,19 @@ namespace Emby.Server.Implementations.LiveTv
var info = new LiveTvInfo
{
Services = services,
- IsEnabled = services.Length > 0
+ IsEnabled = services.Length > 0,
+ EnabledUsers = _userManager.Users
+ .Where(IsLiveTvEnabled)
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .ToArray()
};
- info.EnabledUsers = _userManager.Users
- .Where(IsLiveTvEnabled)
- .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
- .ToArray();
-
return info;
}
private bool IsLiveTvEnabled(User user)
{
- return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
+ return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
}
public IEnumerable<User> GetEnabledUsers()
diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
index 889760586..358606b0d 100644
--- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Querying;
@@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Playlists
}
query.Recursive = true;
- query.IncludeItemTypes = new string[] { "Playlist" };
+ query.IncludeItemTypes = new[] { "Playlist" };
query.Parent = null;
return LibraryManager.GetItemsResult(query);
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 8d022d6c4..9feef5d8d 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -21,6 +22,8 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PlaylistsNET.Content;
using PlaylistsNET.Models;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace Emby.Server.Implementations.Playlists
{
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 506e6739f..07e443ef5 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -7,6 +7,8 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -15,7 +17,6 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@@ -28,7 +29,9 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
namespace Emby.Server.Implementations.Session
{
@@ -283,11 +286,18 @@ namespace Emby.Server.Implementations.Session
if (user != null)
{
var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
- user.LastActivityDate = activityDate;
if ((activityDate - userLastActivityDate).TotalSeconds > 60)
{
- _userManager.UpdateUser(user);
+ try
+ {
+ user.LastActivityDate = activityDate;
+ _userManager.UpdateUser(user);
+ }
+ catch (DbUpdateConcurrencyException e)
+ {
+ _logger.LogWarning(e, "Error updating user's last activity date.");
+ }
}
}
@@ -434,7 +444,13 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
- private SessionInfo GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+ private SessionInfo GetSessionInfo(
+ string appName,
+ string appVersion,
+ string deviceId,
+ string deviceName,
+ string remoteEndPoint,
+ User user)
{
CheckDisposed();
@@ -447,14 +463,13 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
- var sessionInfo = _activeConnections.GetOrAdd(key, k =>
- {
- return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
- });
+ var sessionInfo = _activeConnections.GetOrAdd(
+ key,
+ k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user));
- sessionInfo.UserId = user == null ? Guid.Empty : user.Id;
- sessionInfo.UserName = user?.Name;
- sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+ sessionInfo.UserId = user?.Id ?? Guid.Empty;
+ sessionInfo.UserName = user?.Username;
+ sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
sessionInfo.Client = appName;
@@ -473,7 +488,14 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
- private SessionInfo CreateSession(string key, string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+ private SessionInfo CreateSession(
+ string key,
+ string appName,
+ string appVersion,
+ string deviceId,
+ string deviceName,
+ string remoteEndPoint,
+ User user)
{
var sessionInfo = new SessionInfo(this, _logger)
{
@@ -483,11 +505,11 @@ namespace Emby.Server.Implementations.Session
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
- var username = user?.Name;
+ var username = user?.Username;
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = username;
- sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+ sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
if (string.IsNullOrEmpty(deviceName))
@@ -535,10 +557,7 @@ namespace Emby.Server.Implementations.Session
private void StartIdleCheckTimer()
{
- if (_idleTimer == null)
- {
- _idleTimer = new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
- }
+ _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
private void StopIdleCheckTimer()
@@ -786,7 +805,7 @@ namespace Emby.Server.Implementations.Session
{
var changed = false;
- if (user.Configuration.RememberAudioSelections)
+ if (user.RememberAudioSelections)
{
if (data.AudioStreamIndex != info.AudioStreamIndex)
{
@@ -803,7 +822,7 @@ namespace Emby.Server.Implementations.Session
}
}
- if (user.Configuration.RememberSubtitleSelections)
+ if (user.RememberSubtitleSelections)
{
if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
{
@@ -1114,13 +1133,13 @@ namespace Emby.Server.Implementations.Session
if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
{
throw new ArgumentException(
- string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name));
+ string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username));
}
}
if (user != null
&& command.ItemIds.Length == 1
- && user.Configuration.EnableNextEpisodeAutoPlay
+ && user.EnableNextEpisodeAutoPlay
&& _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
{
var series = episode.Series;
@@ -1191,7 +1210,7 @@ namespace Emby.Server.Implementations.Session
DtoOptions = new DtoOptions(false)
{
EnableImages = false,
- Fields = new ItemFields[]
+ Fields = new[]
{
ItemFields.SortName
}
@@ -1353,7 +1372,7 @@ namespace Emby.Server.Implementations.Session
list.Add(new SessionUserInfo
{
UserId = userId,
- UserName = user.Name
+ UserName = user.Username
});
session.AdditionalUsers = list.ToArray();
@@ -1513,7 +1532,7 @@ namespace Emby.Server.Implementations.Session
DeviceName = deviceName,
UserId = user.Id,
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- UserName = user.Name
+ UserName = user.Username
};
_logger.LogInformation("Creating new access token for user {0}", user.Id);
@@ -1710,15 +1729,15 @@ namespace Emby.Server.Implementations.Session
return info;
}
- private string GetImageCacheTag(BaseItem item, ImageType type)
+ private string GetImageCacheTag(User user)
{
try
{
- return _imageProcessor.GetImageCacheTag(item, type);
+ return _imageProcessor.GetImageCacheTag(user);
}
- catch (Exception ex)
+ catch (Exception e)
{
- _logger.LogError(ex, "Error getting image information for {Type}", type);
+ _logger.LogError(e, "Error getting image information for profile image");
return null;
}
}
@@ -1827,7 +1846,10 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList();
+ var adminUserIds = _userManager.Users
+ .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
+ .Select(i => i.Id)
+ .ToList();
return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
}
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index 5c1503ed2..03ff19d21 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 73f59f8cd..5e527ea25 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index aba14c6ca..0c4e82d01 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index 39d9bc68e..d95948406 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index 478df4035..1632c5a7a 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index eb74ce1bd..afbaaf6ab 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 1bfc9a9a5..ecc3eeb39 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
-using Microsoft.Extensions.Logging;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.SyncPlay
{
@@ -109,14 +109,6 @@ namespace Emby.Server.Implementations.SyncPlay
_disposed = true;
}
- private void CheckDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
- }
-
private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@@ -149,38 +141,24 @@ namespace Emby.Server.Implementations.SyncPlay
var item = _libraryManager.GetItemById(itemId);
// Check ParentalRating access
- var hasParentalRatingAccess = true;
- if (user.Policy.MaxParentalRating.HasValue)
- {
- hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating;
- }
+ var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue
+ || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating;
- if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
+ if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess)
{
var collections = _libraryManager.GetCollectionFolders(item).Select(
- folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
- );
- var intersect = collections.Intersect(user.Policy.EnabledFolders);
- return intersect.Any();
- }
- else
- {
- return hasParentalRatingAccess;
+ folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
+
+ return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
}
+
+ return hasParentalRatingAccess;
}
private Guid? GetSessionGroup(SessionInfo session)
{
- ISyncPlayController group;
- _sessionToGroupMap.TryGetValue(session.Id, out group);
- if (group != null)
- {
- return group.GetGroupId();
- }
- else
- {
- return null;
- }
+ _sessionToGroupMap.TryGetValue(session.Id, out var group);
+ return group?.GetGroupId();
}
/// <inheritdoc />
@@ -188,7 +166,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
+ if (user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
{
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
@@ -196,7 +174,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.CreateGroupDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -219,7 +197,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ if (user.SyncPlayAccess == SyncPlayAccess.None)
{
_logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id);
@@ -227,7 +205,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -244,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.GroupDoesNotExist
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -257,7 +235,7 @@ namespace Emby.Server.Implementations.SyncPlay
GroupId = group.GetGroupId().ToString(),
Type = GroupUpdateType.LibraryAccessDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -281,8 +259,7 @@ namespace Emby.Server.Implementations.SyncPlay
// TODO: determine what happens to users that are in a group and get their permissions revoked
lock (_groupsLock)
{
- ISyncPlayController group;
- _sessionToGroupMap.TryGetValue(session.Id, out group);
+ _sessionToGroupMap.TryGetValue(session.Id, out var group);
if (group == null)
{
@@ -292,7 +269,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -311,7 +288,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ if (user.SyncPlayAccess == SyncPlayAccess.None)
{
return new List<GroupInfoView>();
}
@@ -341,7 +318,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ if (user.SyncPlayAccess == SyncPlayAccess.None)
{
_logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id);
@@ -349,14 +326,13 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
lock (_groupsLock)
{
- ISyncPlayController group;
- _sessionToGroupMap.TryGetValue(session.Id, out group);
+ _sessionToGroupMap.TryGetValue(session.Id, out var group);
if (group == null)
{
@@ -366,7 +342,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -393,8 +369,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Session not in any group!");
}
- ISyncPlayController tempGroup;
- _sessionToGroupMap.Remove(session.Id, out tempGroup);
+ _sessionToGroupMap.Remove(session.Id, out var tempGroup);
if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
{
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 383615f74..6999668d1 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -4,13 +4,17 @@ 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.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.TV
{
@@ -73,7 +77,8 @@ namespace Emby.Server.Implementations.TV
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
+ .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToArray();
}
@@ -191,7 +196,7 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) },
IsPlayed = true,
Limit = 1,
@@ -204,7 +209,6 @@ namespace Emby.Server.Implementations.TV
},
EnableImages = false
}
-
}).FirstOrDefault();
Func<Episode> getEpisode = () =>
@@ -219,7 +223,7 @@ namespace Emby.Server.Implementations.TV
IsPlayed = false,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
- MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName,
+ MinSortName = lastWatchedEpisode?.SortName,
DtoOptions = dtoOptions
}).Cast<Episode>().FirstOrDefault();
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
new file mode 100644
index 000000000..953acac80
--- /dev/null
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -0,0 +1,100 @@
+using System.Net;
+using System.Security.Claims;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth
+{
+ /// <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>
+ /// <returns>Validated claim status.</returns>
+ protected bool ValidateClaims(
+ ClaimsPrincipal claimsPrincipal,
+ bool ignoreSchedule = false,
+ bool localAccessOnly = false)
+ {
+ // Ensure claim has userId.
+ var userId = ClaimHelpers.GetUserId(claimsPrincipal);
+ if (userId == null)
+ {
+ return false;
+ }
+
+ // Ensure userId links to a valid user.
+ var user = _userManager.GetUserById(userId.Value);
+ if (user == null)
+ {
+ return false;
+ }
+
+ // Ensure user is not disabled.
+ if (user.HasPermission(PermissionKind.IsDisabled))
+ {
+ return false;
+ }
+
+ var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
+ var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
+ // User cannot access remotely and user is remote
+ if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
+ {
+ return false;
+ }
+
+ if (localAccessOnly && !isInLocalNetwork)
+ {
+ return false;
+ }
+
+ // User attempting to access out of parental control hours.
+ if (!ignoreSchedule
+ && !user.HasPermission(PermissionKind.IsAdministrator)
+ && !user.IsParentalScheduleAllowed())
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static IPAddress NormalizeIp(IPAddress ip)
+ {
+ return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 100054096..ea02e6a0b 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -1,8 +1,10 @@
+using System.Globalization;
using System.Security.Authentication;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
@@ -38,15 +40,10 @@ namespace Jellyfin.Api.Auth
/// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
- var authenticatedAttribute = new AuthenticatedAttribute
- {
- IgnoreLegacyAuth = true
- };
-
try
{
- var user = _authService.Authenticate(Request, authenticatedAttribute);
- if (user == null)
+ var authorizationInfo = _authService.Authenticate(Request);
+ if (authorizationInfo == null)
{
return Task.FromResult(AuthenticateResult.NoResult());
// TODO return when legacy API is removed.
@@ -56,11 +53,16 @@ namespace Jellyfin.Api.Auth
var claims = new[]
{
- new Claim(ClaimTypes.Name, user.Name),
- new Claim(
- ClaimTypes.Role,
- value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User)
+ new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
+ new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
+ new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
+ new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
+ new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
+ new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
+ new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
};
+
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
new file mode 100644
index 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/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/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
new file mode 100644
index 000000000..9afa0b28f
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+{
+ /// <summary>
+ /// Escape schedule controls handler.
+ /// </summary>
+ public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> 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 IgnoreScheduleHandler(
+ IUserManager userManager,
+ INetworkManager networkManager,
+ IHttpContextAccessor httpContextAccessor)
+ : base(userManager, networkManager, httpContextAccessor)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
+ {
+ var validated = ValidateClaims(context.User, ignoreSchedule: true);
+ if (!validated)
+ {
+ context.Fail();
+ return Task.CompletedTask;
+ }
+
+ context.Succeed(requirement);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
new file mode 100644
index 000000000..d5bb61ce6
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+{
+ /// <summary>
+ /// Escape schedule controls requirement.
+ /// </summary>
+ public class IgnoreScheduleRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
new file mode 100644
index 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..cf574e43d 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -6,6 +6,11 @@ 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";
@@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
/// 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 IgnoreSchedule = "IgnoreSchedule";
}
}
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index 895d9f719..ec50fb022 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,7 +1,5 @@
-#nullable enable
-#pragma warning disable CA1801
-
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
@@ -42,6 +40,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
[HttpGet("Entries")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")]
public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
new file mode 100644
index 000000000..67790c0e4
--- /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")]
+ [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/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 2a1dce74d..d275ed2eb 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Configuration Controller.
/// </summary>
[Route("System")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class ConfigurationController : BaseJellyfinApiController
{
private readonly IServerConfigurationManager _configurationManager;
@@ -53,15 +51,15 @@ namespace Jellyfin.Api.Controllers
/// Updates application configuration.
/// </summary>
/// <param name="configuration">Configuration.</param>
- /// <response code="200">Configuration updated.</response>
+ /// <response code="204">Configuration updated.</response>
/// <returns>Update status.</returns>
[HttpPost("Configuration")]
[Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
{
_configurationManager.ReplaceConfiguration(configuration);
- return Ok();
+ return NoContent();
}
/// <summary>
@@ -70,7 +68,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="key">Configuration key.</param>
/// <response code="200">Configuration returned.</response>
/// <returns>Configuration.</returns>
- [HttpGet("Configuration/{Key}")]
+ [HttpGet("Configuration/{key}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<object> GetNamedConfiguration([FromRoute] string key)
{
@@ -81,17 +79,17 @@ namespace Jellyfin.Api.Controllers
/// Updates named configuration.
/// </summary>
/// <param name="key">Configuration key.</param>
- /// <response code="200">Named configuration updated.</response>
+ /// <response code="204">Named configuration updated.</response>
/// <returns>Update status.</returns>
- [HttpPost("Configuration/{Key}")]
+ [HttpPost("Configuration/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
{
var configurationType = _configurationManager.GetConfigurationType(key);
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false);
_configurationManager.SaveConfiguration(key, configuration);
- return Ok();
+ return NoContent();
}
/// <summary>
@@ -111,15 +109,15 @@ namespace Jellyfin.Api.Controllers
/// Updates the path to the media encoder.
/// </summary>
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
- /// <response code="200">Media encoder path updated.</response>
+ /// <response code="204">Media encoder path updated.</response>
/// <returns>Status.</returns>
[HttpPost("MediaEncoder/Path")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath)
{
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
- return Ok();
+ return NoContent();
}
}
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
new file mode 100644
index 000000000..aab920ff3
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -0,0 +1,275 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using Jellyfin.Api.Models;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Plugins;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The dashboard controller.
+ /// </summary>
+ 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", string.Empty);
+ }
+
+ /// <summary>
+ /// Gets a resource from the web client.
+ /// </summary>
+ /// <param name="resourceName">The resource name.</param>
+ /// <param name="v">The v.</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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")]
+ public ActionResult GetWebClientResource(
+ [FromRoute] string resourceName,
+ [FromQuery] string? v)
+ {
+ if (!_appConfig.HostWebClient() || WebClientUiPath == null)
+ {
+ return NotFound("Server does not host a web client.");
+ }
+
+ var path = resourceName;
+ var basePath = WebClientUiPath;
+
+ // Bounce them to the startup wizard if it hasn't been completed yet
+ if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
+ && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase)
+ && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase))
+ {
+ return Redirect("index.html?start=wizard#!/wizardstart.html");
+ }
+
+ var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read);
+ return File(stream, MimeTypes.GetMimeType(path));
+ }
+
+ /// <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", string.Empty);
+ }
+
+ /// <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
index 1e7557903..55ca7b7c0 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Devices;
@@ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Devices Controller.
/// </summary>
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
@@ -105,12 +103,12 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="id">Device Id.</param>
/// <param name="deviceOptions">Device Options.</param>
- /// <response code="200">Device options updated.</response>
+ /// <response code="204">Device options updated.</response>
/// <response code="404">Device not found.</response>
- /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ /// <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.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
[FromQuery, BindRequired] string id,
@@ -123,18 +121,19 @@ namespace Jellyfin.Api.Controllers
}
_deviceManager.UpdateDeviceOptions(id, deviceOptions);
- return Ok();
+ return NoContent();
}
/// <summary>
/// Deletes a device.
/// </summary>
/// <param name="id">Device Id.</param>
- /// <response code="200">Device deleted.</response>
+ /// <response code="204">Device deleted.</response>
/// <response code="404">Device not found.</response>
- /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpDelete]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
{
var existingDevice = _deviceManager.GetDevice(id);
@@ -150,7 +149,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.Logout(session);
}
- return Ok();
+ return NoContent();
}
}
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
new file mode 100644
index 000000000..3f946d9d2
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -0,0 +1,76 @@
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Display Preferences Controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class DisplayPreferencesController : BaseJellyfinApiController
+ {
+ private readonly IDisplayPreferencesRepository _displayPreferencesRepository;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
+ /// </summary>
+ /// <param name="displayPreferencesRepository">Instance of <see cref="IDisplayPreferencesRepository"/> interface.</param>
+ public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository)
+ {
+ _displayPreferencesRepository = displayPreferencesRepository;
+ }
+
+ /// <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)]
+ public ActionResult<DisplayPreferences> GetDisplayPreferences(
+ [FromRoute] string displayPreferencesId,
+ [FromQuery] [Required] string userId,
+ [FromQuery] [Required] string client)
+ {
+ return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
+ }
+
+ /// <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, BindRequired] string userId,
+ [FromQuery, BindRequired] string client,
+ [FromBody, BindRequired] DisplayPreferences displayPreferences)
+ {
+ _displayPreferencesRepository.SaveDisplayPreferences(
+ displayPreferences,
+ userId,
+ client,
+ CancellationToken.None);
+
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
new file mode 100644
index 000000000..0934a116a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -0,0 +1,221 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+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>
+ [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 == null || userId == Guid.Empty
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ {
+ parentItem = null;
+ }
+
+ var item = string.IsNullOrEmpty(parentId)
+ ? user == null
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder()
+ : parentItem;
+
+ var query = new InternalItemsQuery
+ {
+ User = user,
+ MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ Recursive = true,
+ EnableTotalRecordCount = false,
+ DtoOptions = new DtoOptions
+ {
+ Fields = new[] { ItemFields.Genres, ItemFields.Tags },
+ EnableImages = false,
+ EnableUserData = false
+ }
+ };
+
+ var itemList = ((Folder)item!).GetItemList(query);
+ return new QueryFiltersLegacy
+ {
+ Years = itemList.Select(i => i.ProductionYear ?? -1)
+ .Where(i => i > 0)
+ .Distinct()
+ .OrderBy(i => i)
+ .ToArray(),
+
+ Genres = itemList.SelectMany(i => i.Genres)
+ .DistinctNames()
+ .OrderBy(i => i)
+ .ToArray(),
+
+ Tags = itemList
+ .SelectMany(i => i.Tags)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(i => i)
+ .ToArray(),
+
+ OfficialRatings = itemList
+ .Select(i => i.OfficialRating)
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(i => i)
+ .ToArray()
+ };
+ }
+
+ /// <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="mediaTypes">[Unused] Optional. Filter by MediaType. 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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "mediaTypes", Justification = "Imported from ServiceStack")]
+ public ActionResult<QueryFilters> GetQueryFilters(
+ [FromQuery] Guid? userId,
+ [FromQuery] string? parentId,
+ [FromQuery] string? includeItemTypes,
+ [FromQuery] string? mediaTypes,
+ [FromQuery] bool? isAiring,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? recursive)
+ {
+ var parentItem = string.IsNullOrEmpty(parentId)
+ ? null
+ : _libraryManager.GetItemById(parentId);
+
+ var user = userId == null || userId == Guid.Empty
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ {
+ parentItem = null;
+ }
+
+ var filters = new QueryFilters();
+ var genreQuery = new InternalItemsQuery(user)
+ {
+ IncludeItemTypes =
+ (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ DtoOptions = new DtoOptions
+ {
+ Fields = Array.Empty<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/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
new file mode 100644
index 000000000..4800c0608
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+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] string name, [FromRoute] 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] string theme,
+ [FromRoute] 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] string theme,
+ [FromRoute] 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/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
new file mode 100644
index 000000000..44709d0ee
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -0,0 +1,364 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Item lookup controller.
+ /// </summary>
+ [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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] RemoteSearchResult searchResult,
+ [FromQuery] bool replaceAllImages = true)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ _logger.LogInformation(
+ "Setting provider id's to item {0}-{1}: {2}",
+ item.Id,
+ item.Name,
+ JsonSerializer.Serialize(searchResult.ProviderIds));
+
+ // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
+ item.ProviderIds = searchResult.ProviderIds;
+ await _providerManager.RefreshFullItem(
+ item,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllMetadata = true,
+ ReplaceAllImages = replaceAllImages,
+ SearchResult = searchResult
+ }, CancellationToken.None).ConfigureAwait(false);
+
+ return 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)
+ {
+ var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
+ var ext = result.ContentType.Split('/').Last();
+ var fullCachePath = GetFullCachePath(urlHash + "." + ext);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+ await using (var stream = result.Content)
+ {
+ await using var fileStream = new FileStream(
+ fullCachePath,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.Read,
+ IODefaults.FileStreamBufferSize,
+ true);
+
+ await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+ await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
+ }
+
+ /// <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..e6cdf4edb
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -0,0 +1,90 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+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>
+ /// [Authenticated]
+ [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>
+ /// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")]
+ public ActionResult Post(
+ [FromRoute] Guid itemId,
+ [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
+ [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
+ [FromQuery] bool replaceAllMetadata = false,
+ [FromQuery] bool replaceAllImages = false,
+ [FromQuery] bool recursive = false)
+ {
+ var item = _libraryManager.GetItemById(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..384f250ec 100644
--- a/MediaBrowser.Api/ItemUpdateService.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -1,215 +1,99 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
-namespace MediaBrowser.Api
+namespace Jellyfin.Api.Controllers
{
- [Route("/Items/{ItemId}", "POST", Summary = "Updates an item")]
- public class UpdateItem : BaseItemDto, IReturnVoid
- {
- [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string ItemId { get; set; }
- }
-
- [Route("/Items/{ItemId}/MetadataEditor", "GET", Summary = "Gets metadata editor info for an item")]
- public class GetMetadataEditorInfo : IReturn<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>
+ [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, BindRequired] 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 +116,7 @@ namespace MediaBrowser.Api
if (displayOrderChanged)
{
_providerManager.QueueRefresh(
- series.Id,
+ series!.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
@@ -241,11 +125,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, BindRequired] string contentType)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ var path = item.ContainingFolderPath;
+
+ var types = _serverConfigurationManager.Configuration.ContentTypes
+ .Where(i => !string.IsNullOrWhiteSpace(i.Name))
+ .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (!string.IsNullOrWhiteSpace(contentType))
+ {
+ types.Add(new NameValuePair
+ {
+ Name = path,
+ Value = contentType
+ });
+ }
+
+ _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
+ _serverConfigurationManager.SaveConfiguration();
+ return NoContent();
}
private void UpdateItem(BaseItemDto request, BaseItem item)
@@ -361,24 +335,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 +367,81 @@ namespace MediaBrowser.Api
return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
}
+
+ private DateTime NormalizeDateTime(DateTime val)
+ {
+ return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+ }
+
+ private List<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/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
new file mode 100644
index 000000000..62c547409
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -0,0 +1,341 @@
+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 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>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">Virtual folders retrieved.</response>
+ /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId)
+ {
+ 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="refreshLibrary">Whether to refresh the library.</param>
+ /// <param name="paths">The paths of the virtual folder.</param>
+ /// <param name="libraryOptions">The library options.</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] bool refreshLibrary,
+ [FromQuery] string[] paths,
+ [FromQuery] LibraryOptions libraryOptions)
+ {
+ libraryOptions ??= new LibraryOptions();
+
+ if (paths != null && paths.Length > 0)
+ {
+ libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
+ }
+
+ 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)
+ {
+ 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)
+ {
+ 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="name">The name of the library.</param>
+ /// <param name="path">The path to add.</param>
+ /// <param name="pathInfo">The path info.</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(
+ [FromQuery] string name,
+ [FromQuery] string path,
+ [FromQuery] MediaPathInfo pathInfo,
+ [FromQuery] bool refreshLibrary)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ _libraryMonitor.Stop();
+
+ try
+ {
+ var mediaPath = pathInfo ?? new MediaPathInfo { Path = path };
+
+ _libraryManager.AddMediaPath(name, mediaPath);
+ }
+ finally
+ {
+ Task.Run(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,
+ [FromQuery] 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)
+ {
+ 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,
+ [FromQuery] LibraryOptions libraryOptions)
+ {
+ var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
+
+ collectionFolder.UpdateLibraryOptions(libraryOptions);
+ return NoContent();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
new file mode 100644
index 000000000..1466dd3ec
--- /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.FirstTimeSetupOrElevated)]
+ 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/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 8d82ca10f..f22636489 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,11 +1,10 @@
-#nullable enable
-#pragma warning disable CA1801
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Models.NotificationDtos;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
@@ -43,8 +42,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="limit">An optional limit on the number of notifications returned.</param>
/// <response code="200">Notifications returned.</response>
/// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
- [HttpGet("{UserID}")]
+ [HttpGet("{userId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
public ActionResult<NotificationResultDto> GetNotifications(
[FromRoute] string userId,
[FromQuery] bool? isRead,
@@ -60,8 +63,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">The user's ID.</param>
/// <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")]
+ [HttpGet("{userId}/Summary")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
public ActionResult<NotificationsSummaryDto> GetNotificationsSummary(
[FromRoute] string userId)
{
@@ -99,10 +103,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="description">The description of the notification.</param>
/// <param name="url">The URL of the notification.</param>
/// <param name="level">The level of the notification.</param>
- /// <response code="200">Notification sent.</response>
- /// <returns>An <cref see="OkResult"/>.</returns>
+ /// <response code="204">Notification sent.</response>
+ /// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("Admin")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CreateAdminNotification(
[FromQuery] string name,
[FromQuery] string description,
@@ -115,13 +119,16 @@ namespace Jellyfin.Api.Controllers
Description = description,
Url = url,
Level = level ?? NotificationLevel.Normal,
- UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
+ UserIds = _userManager.Users
+ .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
+ .Select(user => user.Id)
+ .ToArray(),
Date = DateTime.UtcNow,
};
_notificationManager.SendNotification(notification, CancellationToken.None);
- return Ok();
+ return NoContent();
}
/// <summary>
@@ -129,15 +136,17 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The userID.</param>
/// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
- /// <response code="200">Notifications set as read.</response>
- /// <returns>An <cref see="OkResult"/>.</returns>
- [HttpPost("{UserID}/Read")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ /// <response code="204">Notifications set as read.</response>
+ /// <returns>A <cref see="NoContentResult"/>.</returns>
+ [HttpPost("{userId}/Read")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
public ActionResult SetRead(
[FromRoute] string userId,
[FromQuery] string ids)
{
- return Ok();
+ return NoContent();
}
/// <summary>
@@ -145,15 +154,17 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The userID.</param>
/// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
- /// <response code="200">Notifications set as unread.</response>
- /// <returns>An <cref see="OkResult"/>.</returns>
- [HttpPost("{UserID}/Unread")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ /// <response code="204">Notifications set as unread.</response>
+ /// <returns>A <cref see="NoContentResult"/>.</returns>
+ [HttpPost("{userId}/Unread")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
public ActionResult SetUnread(
[FromRoute] string userId,
[FromQuery] string ids)
{
- return Ok();
+ return NoContent();
}
}
}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index f37319c19..486575d23 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Package Controller.
/// </summary>
[Route("Packages")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
@@ -37,9 +35,10 @@ namespace Jellyfin.Api.Controllers
/// </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("/{Name}")]
- [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)]
+ [HttpGet("/{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
[FromRoute] [Required] string name,
[FromQuery] string? assemblyGuid)
@@ -56,9 +55,10 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets available packages.
/// </summary>
+ /// <response code="200">Available packages returned.</response>
/// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
[HttpGet]
- [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IEnumerable<PackageInfo>> GetPackages()
{
IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
@@ -72,11 +72,11 @@ namespace Jellyfin.Api.Controllers
/// <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="200">Package found.</response>
+ /// <response code="204">Package found.</response>
/// <response code="404">Package not found.</response>
- /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
- [HttpPost("/Installed/{Name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
+ [HttpPost("/Installed/{name}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
@@ -98,23 +98,23 @@ namespace Jellyfin.Api.Controllers
await _installationManager.InstallPackage(package).ConfigureAwait(false);
- return Ok();
+ return NoContent();
}
/// <summary>
/// Cancels a package installation.
/// </summary>
- /// <param name="id">Installation Id.</param>
- /// <response code="200">Installation cancelled.</response>
- /// <returns>An <see cref="OkResult"/> on successfully cancelling a package installation.</returns>
- [HttpDelete("/Installing/{id}")]
+ /// <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("/Installing/{packageId}")]
[Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public IActionResult CancelPackageInstallation(
- [FromRoute] [Required] string id)
+ [FromRoute] [Required] Guid packageId)
{
- _installationManager.CancelInstallation(new Guid(id));
-
- return Ok();
+ _installationManager.CancelInstallation(packageId);
+ return NoContent();
}
}
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
new file mode 100644
index 000000000..2dc0d2dc7
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -0,0 +1,199 @@
+using System;
+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;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+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, BindRequired] CreatePlaylistDto createPlaylistRequest)
+ {
+ Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
+ var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
+ {
+ Name = createPlaylistRequest.Name,
+ ItemIdList = idGuidArray,
+ UserId = createPlaylistRequest.UserId,
+ MediaType = createPlaylistRequest.MediaType
+ }).ConfigureAwait(false);
+
+ return result;
+ }
+
+ /// <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);
+ 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/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
new file mode 100644
index 000000000..979d40119
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.PluginDtos;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Plugins;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Plugins controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class PluginsController : BaseJellyfinApiController
+ {
+ private readonly IApplicationHost _appHost;
+ private readonly IInstallationManager _installationManager;
+
+ /// <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>
+ /// <param name="isAppStoreEnabled">Optional. Unused.</param>
+ /// <response code="200">Installed plugins returned.</response>
+ /// <returns>List of currently installed plugins.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")]
+ public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled)
+ {
+ 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)
+ .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, BindRequired] 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..a0d14be7a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+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;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Remote Images Controller.
+ /// </summary>
+ [Route("Images")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class RemoteImageController : BaseJellyfinApiController
+ {
+ private readonly IProviderManager _providerManager;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IHttpClient _httpClient;
+ 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="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public RemoteImageController(
+ IProviderManager providerManager,
+ IServerApplicationPaths applicationPaths,
+ IHttpClient httpClient,
+ ILibraryManager libraryManager)
+ {
+ _providerManager = providerManager;
+ _applicationPaths = applicationPaths;
+ _httpClient = httpClient;
+ _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("{itemId}/RemoteImages")]
+ [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)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ var images = await _providerManager.GetAvailableRemoteImages(
+ item,
+ new RemoteImageQuery(providerName)
+ {
+ IncludeAllLanguages = includeAllLanguages,
+ IncludeDisabledProviders = true,
+ ImageType = type
+ }, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var imageArray = images.ToArray();
+ var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
+ if (type.HasValue)
+ {
+ allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
+ }
+
+ var result = new RemoteImageResult
+ {
+ TotalRecordCount = imageArray.Length,
+ Providers = allProviders.Select(o => o.Name)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray()
+ };
+
+ if (startIndex.HasValue)
+ {
+ imageArray = imageArray.Skip(startIndex.Value).ToArray();
+ }
+
+ if (limit.HasValue)
+ {
+ imageArray = imageArray.Take(limit.Value).ToArray();
+ }
+
+ result.Images = imageArray;
+ return 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("{itemId}/RemoteImages/Providers")]
+ [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("Remote")]
+ [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<FileStreamResult>> GetRemoteImage([FromQuery, BindRequired] 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("{itemId}/RemoteImages/Download")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DownloadRemoteImage(
+ [FromRoute] Guid itemId,
+ [FromQuery, BindRequired] 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)
+ {
+ using var result = await _httpClient.GetResponse(new HttpRequestOptions
+ {
+ Url = url,
+ BufferContent = false
+ }).ConfigureAwait(false);
+ var ext = result.ContentType.Split('/').Last();
+
+ var fullCachePath = GetFullCachePath(urlHash + "." + ext);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+ await using (var stream = result.Content)
+ {
+ await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+ await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
+ .ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index ec05e4fb4..d971889db 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
+using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
/// Search controller.
/// </summary>
[Route("/Search/Hints")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class SearchController : BaseJellyfinApiController
{
private readonly ISearchEngine _searchEngine;
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
new file mode 100644
index 000000000..39da4178d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -0,0 +1,475 @@
+#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>
+ 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.Equals(Guid.Empty))
+ {
+ result = result.Where(i => i.SupportsRemoteControl);
+
+ var user = _userManager.GetUserById(controllableByUserId);
+
+ if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
+ {
+ result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId));
+ }
+
+ 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")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult DisplayContent(
+ [FromRoute] string sessionId,
+ [FromQuery] string itemType,
+ [FromQuery] string itemId,
+ [FromQuery] 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")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult Play(
+ [FromRoute] 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}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendPlaystateCommand(
+ [FromRoute] 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}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendSystemCommand(
+ [FromRoute] string sessionId,
+ [FromRoute] 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}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendGeneralCommand(
+ [FromRoute] string sessionId,
+ [FromRoute] 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")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendFullGeneralCommand(
+ [FromRoute] 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")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SendMessageCommand(
+ [FromRoute] string sessionId,
+ [FromQuery] string text,
+ [FromQuery] string header,
+ [FromQuery] long? timeoutMs)
+ {
+ var command = new MessageCommand
+ {
+ Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
+ TimeoutMs = timeoutMs,
+ Text = text
+ };
+
+ _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_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}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult AddUserToSession(
+ [FromRoute] 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}")]
+ [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")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostCapabilities(
+ [FromQuery] string id,
+ [FromQuery] string playableMediaTypes,
+ [FromQuery] string supportedCommands,
+ [FromQuery] bool supportsMediaControl,
+ [FromQuery] bool supportsSync,
+ [FromQuery] bool supportsPersistentIdentifier = true)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ id = RequestHelpers.GetSession(_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")]
+ [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")]
+ [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")]
+ [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")]
+ [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("/Auto/PasswordResetProviders")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
+ {
+ return _userManager.GetPasswordResetProviders();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 57a02e62a..d96b0f993 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -33,16 +33,16 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Completes the startup wizard.
/// </summary>
- /// <response code="200">Startup wizard completed.</response>
- /// <returns>An <see cref="OkResult"/> indicating success.</returns>
+ /// <response code="204">Startup wizard completed.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Complete")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CompleteWizard()
{
_config.Configuration.IsStartupWizardCompleted = true;
_config.SetOptimalValues();
_config.SaveConfiguration();
- return Ok();
+ return NoContent();
}
/// <summary>
@@ -70,10 +70,10 @@ namespace Jellyfin.Api.Controllers
/// <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="200">Configuration saved.</response>
- /// <returns>An <see cref="OkResult"/> indicating success.</returns>
+ /// <response code="204">Configuration saved.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateInitialConfiguration(
[FromForm] string uiCulture,
[FromForm] string metadataCountryCode,
@@ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers
_config.Configuration.MetadataCountryCode = metadataCountryCode;
_config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
_config.SaveConfiguration();
- return Ok();
+ return NoContent();
}
/// <summary>
@@ -91,16 +91,16 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="enableRemoteAccess">Enable remote access.</param>
/// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
- /// <response code="200">Configuration saved.</response>
- /// <returns>An <see cref="OkResult"/> indicating success.</returns>
+ /// <response code="204">Configuration saved.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoteAccess")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
{
_config.Configuration.EnableRemoteAccess = enableRemoteAccess;
_config.Configuration.EnableUPnP = enableAutomaticPortMapping;
_config.SaveConfiguration();
- return Ok();
+ return NoContent();
}
/// <summary>
@@ -113,35 +113,41 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<StartupUserDto> GetFirstUser()
{
+ // TODO: Remove this method when startup wizard no longer requires an existing user.
+ _userManager.Initialize();
var user = _userManager.Users.First();
- return new StartupUserDto { Name = user.Name, Password = user.Password };
+ return new StartupUserDto
+ {
+ Name = user.Username,
+ Password = user.Password
+ };
}
/// <summary>
/// Sets the user name and password.
/// </summary>
/// <param name="startupUserDto">The DTO containing username and password.</param>
- /// <response code="200">Updated user name and password.</response>
+ /// <response code="204">Updated user name and password.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous update operation.
- /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// The task result contains a <see cref="NoContentResult"/> indicating success.
/// </returns>
[HttpPost("User")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
{
var user = _userManager.Users.First();
- user.Name = startupUserDto.Name;
+ user.Username = startupUserDto.Name;
- _userManager.UpdateUser(user);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
if (!string.IsNullOrEmpty(startupUserDto.Password))
{
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
}
- return Ok();
+ return NoContent();
}
}
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
new file mode 100644
index 000000000..95cc39524
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -0,0 +1,347 @@
+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>
+ 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] 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] 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] 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}")]
+ [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,
+ [FromQuery] bool addVttTimeMap,
+ [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 builder = new StringBuilder();
+
+ var runtime = mediaSource.RunTimeTicks ?? -1;
+
+ if (runtime <= 0)
+ {
+ throw new ArgumentException("HLS Subtitles are not supported for this media.");
+ }
+
+ var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
+ if (segmentLengthTicks <= 0)
+ {
+ throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
+ }
+
+ builder.AppendLine("#EXTM3U");
+ builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture));
+ builder.AppendLine("#EXT-X-VERSION:3");
+ builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+ builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+
+ long positionTicks = 0;
+
+ var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
+
+ while (positionTicks < runtime)
+ {
+ var remaining = runtime - positionTicks;
+ var lengthTicks = Math.Min(remaining, segmentLengthTicks);
+
+ builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
+
+ var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
+
+ var url = string.Format(
+ CultureInfo.CurrentCulture,
+ "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
+ positionTicks.ToString(CultureInfo.InvariantCulture),
+ endPositionTicks.ToString(CultureInfo.InvariantCulture),
+ accessToken);
+
+ builder.AppendLine(url);
+
+ positionTicks += segmentLengthTicks;
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return File(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..e1a99a138
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The suggestions controller.
+ /// </summary>
+ 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="enableTotalRecordCount">Whether to enable the total record count.</param>
+ /// <param name="startIndex">Optional. The start index.</param>
+ /// <param name="limit">Optional. The limit.</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] bool enableTotalRecordCount,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit)
+ {
+ var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+
+ var dtoOptions = new DtoOptions().AddClientFields(Request);
+ var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
+ {
+ OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<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/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
new file mode 100644
index 000000000..e33821b24
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The system controller.
+ /// </summary>
+ [Route("/System")]
+ public class SystemController : BaseJellyfinApiController
+ {
+ private readonly IServerApplicationHost _appHost;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IFileSystem _fileSystem;
+ private readonly INetworkManager _network;
+ private readonly ILogger<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.IgnoreSchedule)]
+ [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+ [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")]
+ [HttpPost("Ping")]
+ [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.LocalAccessOnly)]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [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/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
new file mode 100644
index 000000000..bd950b39f
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using 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.Entities;
+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] 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,
+ EnableTotalRecordCount = enableTotalRecordCount
+ },
+ options);
+
+ var user = _userManager.GetUserById(userId);
+
+ 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] 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 = _userManager.GetUserById(userId);
+
+ 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>
+ /// <param name="sortOrder">Optional. Sort order: Ascending,Descending.</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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "sortOrder", Justification = "Imported from ServiceStack")]
+ public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
+ [FromRoute] string seriesId,
+ [FromQuery] 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,
+ [FromQuery] SortOrder? sortOrder)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ 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] string seriesId,
+ [FromQuery] 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 = _userManager.GetUserById(userId);
+
+ 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/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
new file mode 100644
index 000000000..c1f417df5
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -0,0 +1,552 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+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;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+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>
+ /// <param name="isGuest">Optional filter by IsGuest=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)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")]
+ public ActionResult<IEnumerable<UserDto>> GetUsers(
+ [FromQuery] bool? isHidden,
+ [FromQuery] bool? isDisabled,
+ [FromQuery] bool? isGuest)
+ {
+ 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.IgnoreSchedule)]
+ [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);
+
+ if (user == null)
+ {
+ return NotFound("User not found");
+ }
+
+ _sessionManager.RevokeUserTokens(user.Id, null);
+ _userManager.DeleteUser(user);
+ 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, BindRequired] string pw,
+ [FromQuery, BindRequired] 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, BindRequired] 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.DefaultAuthorization)]
+ [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("/Users/New")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] 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, 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/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 86d9322fe..943ba8af3 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -1,9 +1,8 @@
-#nullable enable
-
using System;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Attachments controller.
/// </summary>
[Route("Videos")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class VideoAttachmentsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
@@ -45,7 +44,7 @@ namespace Jellyfin.Api.Controllers
/// <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}")]
+ [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
new file mode 100644
index 000000000..effe630a9
--- /dev/null
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The videos controller.
+ /// </summary>
+ [Route("Videos")]
+ public class VideosController : BaseJellyfinApiController
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
+ /// <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>
+ public VideosController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
+
+ /// <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.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+
+ var item = itemId.Equals(Guid.Empty)
+ ? (!userId.Equals(Guid.Empty)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.RootFolder)
+ : _libraryManager.GetItemById(itemId);
+
+ var dtoOptions = new DtoOptions();
+ dtoOptions = dtoOptions.AddClientFields(Request);
+
+ BaseItemDto[] items;
+ if (item is Video video)
+ {
+ items = video.GetAdditionalParts()
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
+ .ToArray();
+ }
+ else
+ {
+ items = Array.Empty<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.");
+ }
+
+ 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] 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();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
new file mode 100644
index 000000000..ac248109d
--- /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/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/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 6b2d014a1..6e8882391 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Helpers
{
@@ -104,5 +108,66 @@ namespace Jellyfin.Api.Helpers
? 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 value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(i => new Guid(i))
+ .ToArray();
+ }
}
}
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
index 3706a11e3..3b827ec12 100644
--- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
+++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace Jellyfin.Api.Models.ConfigurationDtos
{
/// <summary>
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/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
index 502b22623..af5239ec2 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using MediaBrowser.Model.Notifications;
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
index e34e176cb..64e92bd83 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
index b3746ee2d..0568dea66 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using MediaBrowser.Model.Notifications;
namespace Jellyfin.Api.Models.NotificationDtos
diff --git a/Jellyfin.Api/Models/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/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.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs
new file mode 100644
index 000000000..32a41368d
--- /dev/null
+++ b/Jellyfin.Data/DayOfWeekHelper.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data
+{
+ public static class DayOfWeekHelper
+ {
+ public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
+ {
+ var days = new List<DayOfWeek>(7);
+
+ if (day == DynamicDayOfWeek.Sunday
+ || day == DynamicDayOfWeek.Weekend
+ || day == DynamicDayOfWeek.Everyday)
+ {
+ days.Add(DayOfWeek.Sunday);
+ }
+
+ if (day == DynamicDayOfWeek.Monday
+ || day == DynamicDayOfWeek.Weekday
+ || day == DynamicDayOfWeek.Everyday)
+ {
+ days.Add(DayOfWeek.Monday);
+ }
+
+ if (day == DynamicDayOfWeek.Tuesday
+ || day == DynamicDayOfWeek.Weekday
+ || day == DynamicDayOfWeek.Everyday)
+ {
+ days.Add(DayOfWeek.Tuesday);
+ }
+
+ if (day == DynamicDayOfWeek.Wednesday
+ || day == DynamicDayOfWeek.Weekday
+ || day == DynamicDayOfWeek.Everyday)
+ {
+ days.Add(DayOfWeek.Wednesday);
+ }
+
+ if (day == DynamicDayOfWeek.Thursday
+ || day == DynamicDayOfWeek.Weekday
+ || day == DynamicDayOfWeek.Everyday)
+ {
+ days.Add(DayOfWeek.Thursday);
+ }
+
+ if (day == DynamicDayOfWeek.Friday
+ || day == DynamicDayOfWeek.Weekday
+ || day == DynamicDayOfWeek.Everyday)
+ {
+ days.Add(DayOfWeek.Friday);
+ }
+
+ if (day == DynamicDayOfWeek.Saturday
+ || day == DynamicDayOfWeek.Weekend
+ || day == DynamicDayOfWeek.Everyday)
+ {
+ days.Add(DayOfWeek.Saturday);
+ }
+
+ return days;
+ }
+ }
+}
diff --git a/Jellyfin.Data/Entities/AccessSchedule.cs b/Jellyfin.Data/Entities/AccessSchedule.cs
new file mode 100644
index 000000000..7d1b76a3f
--- /dev/null
+++ b/Jellyfin.Data/Entities/AccessSchedule.cs
@@ -0,0 +1,91 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using System.Xml.Serialization;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+ /// <summary>
+ /// An entity representing a user's access schedule.
+ /// </summary>
+ public class AccessSchedule
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AccessSchedule"/> class.
+ /// </summary>
+ /// <param name="dayOfWeek">The day of the week.</param>
+ /// <param name="startHour">The start hour.</param>
+ /// <param name="endHour">The end hour.</param>
+ /// <param name="userId">The associated user's id.</param>
+ public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
+ {
+ UserId = userId;
+ DayOfWeek = dayOfWeek;
+ StartHour = startHour;
+ EndHour = endHour;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AccessSchedule"/> class.
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected AccessSchedule()
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the id of this instance.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [XmlIgnore]
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated user.
+ /// </summary>
+ [XmlIgnore]
+ [Required]
+ public Guid UserId { get; protected set; }
+
+ /// <summary>
+ /// Gets or sets the day of week.
+ /// </summary>
+ /// <value>The day of week.</value>
+ [Required]
+ public DynamicDayOfWeek DayOfWeek { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start hour.
+ /// </summary>
+ /// <value>The start hour.</value>
+ [Required]
+ public double StartHour { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end hour.
+ /// </summary>
+ /// <value>The end hour.</value>
+ [Required]
+ public double EndHour { get; set; }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="dayOfWeek">The day of the week.</param>
+ /// <param name="startHour">The start hour.</param>
+ /// <param name="endHour">The end hour.</param>
+ /// <param name="userId">The associated user's id.</param>
+ /// <returns>The newly created instance.</returns>
+ public static AccessSchedule Create(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
+ {
+ return new AccessSchedule(dayOfWeek, startHour, endHour, userId);
+ }
+ }
+}
diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs
index 54f9f4905..5cbb126f9 100644
--- a/Jellyfin.Data/Entities/Group.cs
+++ b/Jellyfin.Data/Entities/Group.cs
@@ -2,19 +2,32 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
- public partial class Group
+ /// <summary>
+ /// An entity representing a group.
+ /// </summary>
+ public partial class Group : IHasPermissions, ISavingChanges
{
- partial void Init();
-
/// <summary>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// Initializes a new instance of the <see cref="Group"/> class.
+ /// Public constructor with required data.
/// </summary>
- protected Group()
+ /// <param name="name">The name of the group.</param>
+ public Group(string name)
{
- GroupPermissions = new HashSet<Permission>();
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ Name = name;
+ Id = Guid.NewGuid();
+
+ Permissions = new HashSet<Permission>();
ProviderMappings = new HashSet<ProviderMapping>();
Preferences = new HashSet<Preference>();
@@ -22,66 +35,45 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
- /// </summary>
- public static Group CreateGroupUnsafe()
- {
- return new Group();
- }
-
- /// <summary>
- /// Public constructor with required data
+ /// Initializes a new instance of the <see cref="Group"/> class.
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
- /// <param name="name"></param>
- /// <param name="_user0"></param>
- public Group(string name, User _user0)
+ protected Group()
{
- if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
- this.Name = name;
-
- if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
- _user0.Groups.Add(this);
-
- this.GroupPermissions = new HashSet<Permission>();
- this.ProviderMappings = new HashSet<ProviderMapping>();
- this.Preferences = new HashSet<Preference>();
-
Init();
}
- /// <summary>
- /// Static create function (for use in LINQ queries, etc.)
- /// </summary>
- /// <param name="name"></param>
- /// <param name="_user0"></param>
- public static Group Create(string name, User _user0)
- {
- return new Group(name, _user0);
- }
-
/*************************************************************************
* Properties
*************************************************************************/
/// <summary>
- /// Identity, Indexed, Required
+ /// Gets or sets the id of this group.
/// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
[Key]
[Required]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public Guid Id { get; protected set; }
/// <summary>
- /// Required, Max length = 255
+ /// Gets or sets the group's name.
/// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string Name { get; set; }
/// <summary>
- /// Required, ConcurrenyToken
+ /// Gets or sets the row version.
/// </summary>
+ /// <remarks>
+ /// Required, Concurrency Token.
+ /// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
@@ -96,7 +88,7 @@ namespace Jellyfin.Data.Entities
*************************************************************************/
[ForeignKey("Permission_GroupPermissions_Id")]
- public virtual ICollection<Permission> GroupPermissions { get; protected set; }
+ public virtual ICollection<Permission> Permissions { get; protected set; }
[ForeignKey("ProviderMapping_ProviderMappings_Id")]
public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
@@ -104,6 +96,27 @@ namespace Jellyfin.Data.Entities
[ForeignKey("Preference_Preferences_Id")]
public virtual ICollection<Preference> Preferences { get; protected set; }
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="name">The name of this group</param>
+ public static Group Create(string name)
+ {
+ return new Group(name);
+ }
+
+ /// <inheritdoc/>
+ public bool HasPermission(PermissionKind kind)
+ {
+ return Permissions.First(p => p.Kind == kind).Value;
+ }
+
+ /// <inheritdoc/>
+ public void SetPermission(PermissionKind kind, bool value)
+ {
+ Permissions.First(p => p.Kind == kind).Value = value;
+ }
+
+ partial void Init();
}
}
-
diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Data/Entities/ImageInfo.cs
new file mode 100644
index 000000000..64e36a791
--- /dev/null
+++ b/Jellyfin.Data/Entities/ImageInfo.cs
@@ -0,0 +1,30 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public class ImageInfo
+ {
+ public ImageInfo(string path)
+ {
+ Path = path;
+ LastModified = DateTime.UtcNow;
+ }
+
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ public Guid? UserId { get; protected set; }
+
+ [Required]
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string Path { get; set; }
+
+ [Required]
+ public DateTime LastModified { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs
index 0b5b52cbd..b675e911d 100644
--- a/Jellyfin.Data/Entities/Permission.cs
+++ b/Jellyfin.Data/Entities/Permission.cs
@@ -1,64 +1,35 @@
-using System;
-using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using System.Runtime.CompilerServices;
+using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
- public partial class Permission
+ /// <summary>
+ /// An entity representing whether the associated user has a specific permission.
+ /// </summary>
+ public partial class Permission : ISavingChanges
{
- partial void Init();
-
- /// <summary>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </summary>
- protected Permission()
- {
- Init();
- }
-
/// <summary>
- /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// Initializes a new instance of the <see cref="Permission"/> class.
+ /// Public constructor with required data.
/// </summary>
- public static Permission CreatePermissionUnsafe()
+ /// <param name="kind">The permission kind.</param>
+ /// <param name="value">The value of this permission.</param>
+ public Permission(PermissionKind kind, bool value)
{
- return new Permission();
- }
-
- /// <summary>
- /// Public constructor with required data
- /// </summary>
- /// <param name="kind"></param>
- /// <param name="value"></param>
- /// <param name="_user0"></param>
- /// <param name="_group1"></param>
- public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
- {
- this.Kind = kind;
-
- this.Value = value;
-
- if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
- _user0.Permissions.Add(this);
-
- if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
- _group1.GroupPermissions.Add(this);
-
+ Kind = kind;
+ Value = value;
Init();
}
/// <summary>
- /// Static create function (for use in LINQ queries, etc.)
+ /// Initializes a new instance of the <see cref="Permission"/> class.
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
- /// <param name="kind"></param>
- /// <param name="value"></param>
- /// <param name="_user0"></param>
- /// <param name="_group1"></param>
- public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
+ protected Permission()
{
- return new Permission(kind, value, _user0, _group1);
+ Init();
}
/*************************************************************************
@@ -66,79 +37,61 @@ namespace Jellyfin.Data.Entities
*************************************************************************/
/// <summary>
- /// Identity, Indexed, Required
+ /// Gets or sets the id of this permission.
/// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
- /// Backing field for Kind
- /// </summary>
- protected Enums.PermissionKind _Kind;
- /// <summary>
- /// When provided in a partial class, allows value of Kind to be changed before setting.
- /// </summary>
- partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue);
- /// <summary>
- /// When provided in a partial class, allows value of Kind to be changed before returning.
- /// </summary>
- partial void GetKind(ref Enums.PermissionKind result);
-
- /// <summary>
- /// Required
+ /// Gets or sets the type of this permission.
/// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
[Required]
- public Enums.PermissionKind Kind
- {
- get
- {
- Enums.PermissionKind value = _Kind;
- GetKind(ref value);
- return (_Kind = value);
- }
- set
- {
- Enums.PermissionKind oldValue = _Kind;
- SetKind(oldValue, ref value);
- if (oldValue != value)
- {
- _Kind = value;
- OnPropertyChanged();
- }
- }
- }
+ public PermissionKind Kind { get; protected set; }
/// <summary>
- /// Required
+ /// Gets or sets a value indicating whether the associated user has this permission.
/// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
[Required]
public bool Value { get; set; }
/// <summary>
- /// Required, ConcurrenyToken
+ /// Gets or sets the row version.
/// </summary>
+ /// <remarks>
+ /// Required, ConcurrencyToken.
+ /// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
- public void OnSavingChanges()
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="kind">The permission kind.</param>
+ /// <param name="value">The value of this permission.</param>
+ /// <returns>The newly created instance.</returns>
+ public static Permission Create(PermissionKind kind, bool value)
{
- RowVersion++;
+ return new Permission(kind, value);
}
- /*************************************************************************
- * Navigation properties
- *************************************************************************/
-
- public virtual event PropertyChangedEventHandler PropertyChanged;
-
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ /// <inheritdoc/>
+ public void OnSavingChanges()
{
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ RowVersion++;
}
+ partial void Init();
}
}
-
diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs
index 505f52e6b..0ca9d7eff 100644
--- a/Jellyfin.Data/Entities/Preference.cs
+++ b/Jellyfin.Data/Entities/Preference.cs
@@ -1,63 +1,33 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
- public partial class Preference
+ /// <summary>
+ /// An entity representing a preference attached to a user or group.
+ /// </summary>
+ public class Preference : ISavingChanges
{
- partial void Init();
-
- /// <summary>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </summary>
- protected Preference()
- {
- Init();
- }
-
/// <summary>
- /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// Initializes a new instance of the <see cref="Preference"/> class.
+ /// Public constructor with required data.
/// </summary>
- public static Preference CreatePreferenceUnsafe()
+ /// <param name="kind">The preference kind.</param>
+ /// <param name="value">The value.</param>
+ public Preference(PreferenceKind kind, string value)
{
- return new Preference();
- }
-
- /// <summary>
- /// Public constructor with required data
- /// </summary>
- /// <param name="kind"></param>
- /// <param name="value"></param>
- /// <param name="_user0"></param>
- /// <param name="_group1"></param>
- public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
- {
- this.Kind = kind;
-
- if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
- this.Value = value;
-
- if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
- _user0.Preferences.Add(this);
-
- if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
- _group1.Preferences.Add(this);
-
-
- Init();
+ Kind = kind;
+ Value = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
- /// Static create function (for use in LINQ queries, etc.)
+ /// Initializes a new instance of the <see cref="Preference"/> class.
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
- /// <param name="kind"></param>
- /// <param name="value"></param>
- /// <param name="_user0"></param>
- /// <param name="_group1"></param>
- public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
+ protected Preference()
{
- return new Preference(kind, value, _user0, _group1);
}
/*************************************************************************
@@ -65,43 +35,61 @@ namespace Jellyfin.Data.Entities
*************************************************************************/
/// <summary>
- /// Identity, Indexed, Required
+ /// Gets or sets the id of this preference.
/// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
- /// Required
+ /// Gets or sets the type of this preference.
/// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
[Required]
- public Enums.PreferenceKind Kind { get; set; }
+ public PreferenceKind Kind { get; protected set; }
/// <summary>
- /// Required, Max length = 65535
+ /// Gets or sets the value of this preference.
/// </summary>
+ /// <remarks>
+ /// Required, Max length = 65535.
+ /// </remarks>
[Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Value { get; set; }
/// <summary>
- /// Required, ConcurrenyToken
+ /// Gets or sets the row version.
/// </summary>
+ /// <remarks>
+ /// Required, ConcurrencyToken.
+ /// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="kind">The preference kind.</param>
+ /// <param name="value">The value.</param>
+ /// <returns>The new instance.</returns>
+ public static Preference Create(PreferenceKind kind, string value)
+ {
+ return new Preference(kind, value);
+ }
+
+ /// <inheritdoc/>
public void OnSavingChanges()
{
RowVersion++;
}
-
- /*************************************************************************
- * Navigation properties
- *************************************************************************/
-
}
}
-
diff --git a/Jellyfin.Data/Entities/ProviderMapping.cs b/Jellyfin.Data/Entities/ProviderMapping.cs
index 6197bd97b..e479341ad 100644
--- a/Jellyfin.Data/Entities/ProviderMapping.cs
+++ b/Jellyfin.Data/Entities/ProviderMapping.cs
@@ -43,12 +43,6 @@ namespace Jellyfin.Data.Entities
if (string.IsNullOrEmpty(providerdata)) throw new ArgumentNullException(nameof(providerdata));
this.ProviderData = providerdata;
- if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
- _user0.ProviderMappings.Add(this);
-
- if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
- _group1.ProviderMappings.Add(this);
-
Init();
}
diff --git a/Jellyfin.Data/Entities/Series.cs b/Jellyfin.Data/Entities/Series.cs
index 097b9958e..4f25c38b7 100644
--- a/Jellyfin.Data/Entities/Series.cs
+++ b/Jellyfin.Data/Entities/Series.cs
@@ -20,14 +20,6 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
- /// </summary>
- public static Series CreateSeriesUnsafe()
- {
- return new Series();
- }
-
- /// <summary>
/// Public constructor with required data
/// </summary>
/// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
@@ -57,27 +49,27 @@ namespace Jellyfin.Data.Entities
/// <summary>
/// Backing field for AirsDayOfWeek
/// </summary>
- protected Enums.Weekday? _AirsDayOfWeek;
+ protected DayOfWeek? _AirsDayOfWeek;
/// <summary>
/// When provided in a partial class, allows value of AirsDayOfWeek to be changed before setting.
/// </summary>
- partial void SetAirsDayOfWeek(Enums.Weekday? oldValue, ref Enums.Weekday? newValue);
+ partial void SetAirsDayOfWeek(DayOfWeek? oldValue, ref DayOfWeek? newValue);
/// <summary>
/// When provided in a partial class, allows value of AirsDayOfWeek to be changed before returning.
/// </summary>
- partial void GetAirsDayOfWeek(ref Enums.Weekday? result);
+ partial void GetAirsDayOfWeek(ref DayOfWeek? result);
- public Enums.Weekday? AirsDayOfWeek
+ public DayOfWeek? AirsDayOfWeek
{
get
{
- Enums.Weekday? value = _AirsDayOfWeek;
+ DayOfWeek? value = _AirsDayOfWeek;
GetAirsDayOfWeek(ref value);
return (_AirsDayOfWeek = value);
}
set
{
- Enums.Weekday? oldValue = _AirsDayOfWeek;
+ DayOfWeek? oldValue = _AirsDayOfWeek;
SetAirsDayOfWeek(oldValue, ref value);
if (oldValue != value)
{
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index a81d5215b..b89b0a8f4 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -2,234 +2,506 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
- public partial class User
+ /// <summary>
+ /// An entity representing a user.
+ /// </summary>
+ public partial class User : IHasPermissions, ISavingChanges
{
- partial void Init();
+ /// <summary>
+ /// The values being delimited here are Guids, so commas work as they do not appear in Guids.
+ /// </summary>
+ private const char Delimiter = ',';
/// <summary>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// Initializes a new instance of the <see cref="User"/> class.
+ /// Public constructor with required data.
/// </summary>
- protected User()
+ /// <param name="username">The username for the new user.</param>
+ /// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
+ /// <param name="passwordResetProviderId">The Id of the user's password reset provider.</param>
+ public User(string username, string authenticationProviderId, string passwordResetProviderId)
{
- Groups = new HashSet<Group>();
+ if (string.IsNullOrEmpty(username))
+ {
+ throw new ArgumentNullException(nameof(username));
+ }
+
+ if (string.IsNullOrEmpty(authenticationProviderId))
+ {
+ throw new ArgumentNullException(nameof(authenticationProviderId));
+ }
+
+ if (string.IsNullOrEmpty(passwordResetProviderId))
+ {
+ throw new ArgumentNullException(nameof(passwordResetProviderId));
+ }
+
+ Username = username;
+ AuthenticationProviderId = authenticationProviderId;
+ PasswordResetProviderId = passwordResetProviderId;
+
+ AccessSchedules = new HashSet<AccessSchedule>();
+ // Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
- ProviderMappings = new HashSet<ProviderMapping>();
Preferences = new HashSet<Preference>();
-
+ // ProviderMappings = new HashSet<ProviderMapping>();
+
+ // Set default values
+ Id = Guid.NewGuid();
+ InvalidLoginAttemptCount = 0;
+ EnableUserPreferenceAccess = true;
+ MustUpdatePassword = false;
+ DisplayMissingEpisodes = false;
+ DisplayCollectionsView = false;
+ HidePlayedInLatest = true;
+ RememberAudioSelections = true;
+ RememberSubtitleSelections = true;
+ EnableNextEpisodeAutoPlay = true;
+ EnableAutoLogin = false;
+ PlayDefaultAudioTrack = true;
+ SubtitleMode = SubtitlePlaybackMode.Default;
+ SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
+
+ AddDefaultPermissions();
+ AddDefaultPreferences();
Init();
}
/// <summary>
- /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
- /// </summary>
- public static User CreateUserUnsafe()
- {
- return new User();
- }
-
- /// <summary>
- /// Public constructor with required data
+ /// Initializes a new instance of the <see cref="User"/> class.
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
- /// <param name="username"></param>
- /// <param name="mustupdatepassword"></param>
- /// <param name="audiolanguagepreference"></param>
- /// <param name="authenticationproviderid"></param>
- /// <param name="invalidloginattemptcount"></param>
- /// <param name="subtitlemode"></param>
- /// <param name="playdefaultaudiotrack"></param>
- public User(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
+ protected User()
{
- if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
- this.Username = username;
-
- this.MustUpdatePassword = mustupdatepassword;
-
- if (string.IsNullOrEmpty(audiolanguagepreference)) throw new ArgumentNullException(nameof(audiolanguagepreference));
- this.AudioLanguagePreference = audiolanguagepreference;
-
- if (string.IsNullOrEmpty(authenticationproviderid)) throw new ArgumentNullException(nameof(authenticationproviderid));
- this.AuthenticationProviderId = authenticationproviderid;
-
- this.InvalidLoginAttemptCount = invalidloginattemptcount;
-
- if (string.IsNullOrEmpty(subtitlemode)) throw new ArgumentNullException(nameof(subtitlemode));
- this.SubtitleMode = subtitlemode;
-
- this.PlayDefaultAudioTrack = playdefaultaudiotrack;
-
- this.Groups = new HashSet<Group>();
- this.Permissions = new HashSet<Permission>();
- this.ProviderMappings = new HashSet<ProviderMapping>();
- this.Preferences = new HashSet<Preference>();
-
Init();
}
- /// <summary>
- /// Static create function (for use in LINQ queries, etc.)
- /// </summary>
- /// <param name="username"></param>
- /// <param name="mustupdatepassword"></param>
- /// <param name="audiolanguagepreference"></param>
- /// <param name="authenticationproviderid"></param>
- /// <param name="invalidloginattemptcount"></param>
- /// <param name="subtitlemode"></param>
- /// <param name="playdefaultaudiotrack"></param>
- public static User Create(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
- {
- return new User(username, mustupdatepassword, audiolanguagepreference, authenticationproviderid, invalidloginattemptcount, subtitlemode, playdefaultaudiotrack);
- }
-
/*************************************************************************
* Properties
*************************************************************************/
/// <summary>
- /// Identity, Indexed, Required
+ /// Gets or sets the Id of the user.
/// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
[Key]
[Required]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ [JsonIgnore]
+ public Guid Id { get; set; }
/// <summary>
- /// Required, Max length = 255
+ /// Gets or sets the user's name.
/// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string Username { get; set; }
/// <summary>
- /// Max length = 65535
+ /// Gets or sets the user's password, or <c>null</c> if none is set.
/// </summary>
+ /// <remarks>
+ /// Max length = 65535.
+ /// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
public string Password { get; set; }
/// <summary>
- /// Required
+ /// Gets or sets the user's easy password, or <c>null</c> if none is set.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string EasyPassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user must update their password.
/// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
[Required]
public bool MustUpdatePassword { get; set; }
/// <summary>
- /// Required, Max length = 255
+ /// Gets or sets the audio language preference.
/// </summary>
- [Required]
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
[MaxLength(255)]
[StringLength(255)]
public string AudioLanguagePreference { get; set; }
/// <summary>
- /// Required, Max length = 255
+ /// Gets or sets the authentication provider id.
/// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string AuthenticationProviderId { get; set; }
/// <summary>
- /// Max length = 65535
+ /// Gets or sets the password reset provider id.
/// </summary>
- [MaxLength(65535)]
- [StringLength(65535)]
- public string GroupedFolders { get; set; }
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string PasswordResetProviderId { get; set; }
/// <summary>
- /// Required
+ /// Gets or sets the invalid login attempt count.
/// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
[Required]
public int InvalidLoginAttemptCount { get; set; }
/// <summary>
- /// Max length = 65535
+ /// Gets or sets the last activity date.
/// </summary>
- [MaxLength(65535)]
- [StringLength(65535)]
- public string LatestItemExcludes { get; set; }
-
- public int? LoginAttemptsBeforeLockout { get; set; }
+ public DateTime? LastActivityDate { get; set; }
/// <summary>
- /// Max length = 65535
+ /// Gets or sets the last login date.
/// </summary>
- [MaxLength(65535)]
- [StringLength(65535)]
- public string MyMediaExcludes { get; set; }
+ public DateTime? LastLoginDate { get; set; }
/// <summary>
- /// Max length = 65535
+ /// Gets or sets the number of login attempts the user can make before they are locked out.
/// </summary>
- [MaxLength(65535)]
- [StringLength(65535)]
- public string OrderedViews { get; set; }
+ public int? LoginAttemptsBeforeLockout { get; set; }
/// <summary>
- /// Required, Max length = 255
+ /// Gets or sets the subtitle mode.
/// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
[Required]
- [MaxLength(255)]
- [StringLength(255)]
- public string SubtitleMode { get; set; }
+ public SubtitlePlaybackMode SubtitleMode { get; set; }
/// <summary>
- /// Required
+ /// Gets or sets a value indicating whether the default audio track should be played.
/// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
[Required]
public bool PlayDefaultAudioTrack { get; set; }
/// <summary>
- /// Max length = 255
+ /// Gets or sets the subtitle language preference.
/// </summary>
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
[MaxLength(255)]
[StringLength(255)]
- public string SubtitleLanguagePrefernce { get; set; }
+ public string SubtitleLanguagePreference { get; set; }
- public bool? DisplayMissingEpisodes { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether missing episodes should be displayed.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool DisplayMissingEpisodes { get; set; }
- public bool? DisplayCollectionsView { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to display the collections view.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool DisplayCollectionsView { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user has a local password.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool EnableLocalPassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the server should hide played content in "Latest".
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool HidePlayedInLatest { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to remember audio selections on played content.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool RememberAudioSelections { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to remember subtitle selections on played content.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool RememberSubtitleSelections { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable auto-play for the next episode.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool EnableNextEpisodeAutoPlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user should auto-login.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool EnableAutoLogin { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user can change their preferences.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [Required]
+ public bool EnableUserPreferenceAccess { get; set; }
- public bool? HidePlayedInLatest { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum parental age rating.
+ /// </summary>
+ public int? MaxParentalAgeRating { get; set; }
- public bool? RememberAudioSelections { get; set; }
+ /// <summary>
+ /// Gets or sets the remote client bitrate limit.
+ /// </summary>
+ public int? RemoteClientBitrateLimit { get; set; }
- public bool? RememberSubtitleSelections { get; set; }
+ /// <summary>
+ /// Gets or sets the internal id.
+ /// This is a temporary stopgap for until the library db is migrated.
+ /// This corresponds to the value of the index of this user in the library db.
+ /// </summary>
+ [Required]
+ public long InternalId { get; set; }
- public bool? EnableNextEpisodeAutoPlay { get; set; }
+ /// <summary>
+ /// Gets or sets the user's profile image. Can be <c>null</c>.
+ /// </summary>
+ // [ForeignKey("UserId")]
+ public virtual ImageInfo ProfileImage { get; set; }
- public bool? EnableUserPreferenceAccess { get; set; }
+ [Required]
+ public SyncPlayAccess SyncPlayAccess { get; set; }
/// <summary>
- /// Required, ConcurrenyToken
+ /// Gets or sets the row version.
/// </summary>
+ /// <remarks>
+ /// Required, Concurrency Token.
+ /// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
- public void OnSavingChanges()
- {
- RowVersion++;
- }
-
/*************************************************************************
* Navigation properties
*************************************************************************/
- [ForeignKey("Group_Groups_Id")]
+
+ /// <summary>
+ /// Gets or sets the list of access schedules this user has.
+ /// </summary>
+ public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
+
+ /*
+ /// <summary>
+ /// Gets or sets the list of groups this user is a member of.
+ /// </summary>
+ [ForeignKey("Group_Groups_Guid")]
public virtual ICollection<Group> Groups { get; protected set; }
+ */
- [ForeignKey("Permission_Permissions_Id")]
+ /// <summary>
+ /// Gets or sets the list of permissions this user has.
+ /// </summary>
+ [ForeignKey("Permission_Permissions_Guid")]
public virtual ICollection<Permission> Permissions { get; protected set; }
+ /*
+ /// <summary>
+ /// Gets or sets the list of provider mappings this user has.
+ /// </summary>
[ForeignKey("ProviderMapping_ProviderMappings_Id")]
public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
+ */
- [ForeignKey("Preference_Preferences_Id")]
+ /// <summary>
+ /// Gets or sets the list of preferences this user has.
+ /// </summary>
+ [ForeignKey("Preference_Preferences_Guid")]
public virtual ICollection<Preference> Preferences { get; protected set; }
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="username">The username for the created user.</param>
+ /// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
+ /// <param name="passwordResetProviderId">The Id of the user's password reset provider.</param>
+ /// <returns>The created instance.</returns>
+ public static User Create(string username, string authenticationProviderId, string passwordResetProviderId)
+ {
+ return new User(username, authenticationProviderId, passwordResetProviderId);
+ }
+
+ /// <inheritdoc/>
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /// <summary>
+ /// Checks whether the user has the specified permission.
+ /// </summary>
+ /// <param name="kind">The permission kind.</param>
+ /// <returns><c>True</c> if the user has the specified permission.</returns>
+ public bool HasPermission(PermissionKind kind)
+ {
+ return Permissions.First(p => p.Kind == kind).Value;
+ }
+
+ /// <summary>
+ /// Sets the given permission kind to the provided value.
+ /// </summary>
+ /// <param name="kind">The permission kind.</param>
+ /// <param name="value">The value to set.</param>
+ public void SetPermission(PermissionKind kind, bool value)
+ {
+ Permissions.First(p => p.Kind == kind).Value = value;
+ }
+
+ /// <summary>
+ /// Gets the user's preferences for the given preference kind.
+ /// </summary>
+ /// <param name="preference">The preference kind.</param>
+ /// <returns>A string array containing the user's preferences.</returns>
+ public string[] GetPreference(PreferenceKind preference)
+ {
+ var val = Preferences.First(p => p.Kind == preference).Value;
+
+ return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
+ }
+
+ /// <summary>
+ /// Sets the specified preference to the given value.
+ /// </summary>
+ /// <param name="preference">The preference kind.</param>
+ /// <param name="values">The values.</param>
+ public void SetPreference(PreferenceKind preference, string[] values)
+ {
+ Preferences.First(p => p.Kind == preference).Value
+ = string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values);
+ }
+
+ /// <summary>
+ /// Checks whether this user is currently allowed to use the server.
+ /// </summary>
+ /// <returns><c>True</c> if the current time is within an access schedule, or there are no access schedules.</returns>
+ public bool IsParentalScheduleAllowed()
+ {
+ return AccessSchedules.Count == 0
+ || AccessSchedules.Any(i => IsParentalScheduleAllowed(i, DateTime.UtcNow));
+ }
+
+ /// <summary>
+ /// Checks whether the provided folder is in this user's grouped folders.
+ /// </summary>
+ /// <param name="id">The Guid of the folder.</param>
+ /// <returns><c>True</c> if the folder is in the user's grouped folders.</returns>
+ public bool IsFolderGrouped(Guid id)
+ {
+ return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id);
+ }
+
+ private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
+ {
+ var localTime = date.ToLocalTime();
+ var hour = localTime.TimeOfDay.TotalHours;
+
+ return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
+ && hour >= schedule.StartHour
+ && hour <= schedule.EndHour;
+ }
+
+ // TODO: make these user configurable?
+ private void AddDefaultPermissions()
+ {
+ Permissions.Add(new Permission(PermissionKind.IsAdministrator, false));
+ Permissions.Add(new Permission(PermissionKind.IsDisabled, false));
+ Permissions.Add(new Permission(PermissionKind.IsHidden, true));
+ Permissions.Add(new Permission(PermissionKind.EnableAllChannels, true));
+ Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true));
+ Permissions.Add(new Permission(PermissionKind.EnableAllFolders, true));
+ Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false));
+ Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true));
+ Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true));
+ Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true));
+ Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true));
+ Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true));
+ Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true));
+ Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
+ Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
+ Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
+ Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
+ Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
+ Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
+ Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
+ Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
+ }
+
+ private void AddDefaultPreferences()
+ {
+ foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>())
+ {
+ Preferences.Add(new Preference(val, string.Empty));
+ }
+ }
+
+ partial void Init();
}
}
-
diff --git a/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs b/Jellyfin.Data/Enums/DynamicDayOfWeek.cs
index 71b16cfba..a33cd9d1c 100644
--- a/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs
+++ b/Jellyfin.Data/Enums/DynamicDayOfWeek.cs
@@ -1,6 +1,6 @@
#pragma warning disable CS1591
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
{
public enum DynamicDayOfWeek
{
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 1506471e8..7d5200874 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -1,26 +1,113 @@
namespace Jellyfin.Data.Enums
{
+ /// <summary>
+ /// The types of user permissions.
+ /// </summary>
public enum PermissionKind
{
- IsAdministrator,
- IsHidden,
- IsDisabled,
- BlockUnrateditems,
- EnbleSharedDeviceControl,
- EnableRemoteAccess,
- EnableLiveTvManagement,
- EnableLiveTvAccess,
- EnableMediaPlayback,
- EnableAudioPlaybackTranscoding,
- EnableVideoPlaybackTranscoding,
- EnableContentDeletion,
- EnableContentDownloading,
- EnableSyncTranscoding,
- EnableMediaConversion,
- EnableAllDevices,
- EnableAllChannels,
- EnableAllFolders,
- EnablePublicSharing,
- AccessSchedules
+ /// <summary>
+ /// Whether the user is an administrator.
+ /// </summary>
+ IsAdministrator = 0,
+
+ /// <summary>
+ /// Whether the user is hidden.
+ /// </summary>
+ IsHidden = 1,
+
+ /// <summary>
+ /// Whether the user is disabled.
+ /// </summary>
+ IsDisabled = 2,
+
+ /// <summary>
+ /// Whether the user can control shared devices.
+ /// </summary>
+ EnableSharedDeviceControl = 3,
+
+ /// <summary>
+ /// Whether the user can access the server remotely.
+ /// </summary>
+ EnableRemoteAccess = 4,
+
+ /// <summary>
+ /// Whether the user can manage live tv.
+ /// </summary>
+ EnableLiveTvManagement = 5,
+
+ /// <summary>
+ /// Whether the user can access live tv.
+ /// </summary>
+ EnableLiveTvAccess = 6,
+
+ /// <summary>
+ /// Whether the user can play media.
+ /// </summary>
+ EnableMediaPlayback = 7,
+
+ /// <summary>
+ /// Whether the server should transcode audio for the user if requested.
+ /// </summary>
+ EnableAudioPlaybackTranscoding = 8,
+
+ /// <summary>
+ /// Whether the server should transcode video for the user if requested.
+ /// </summary>
+ EnableVideoPlaybackTranscoding = 9,
+
+ /// <summary>
+ /// Whether the user can delete content.
+ /// </summary>
+ EnableContentDeletion = 10,
+
+ /// <summary>
+ /// Whether the user can download content.
+ /// </summary>
+ EnableContentDownloading = 11,
+
+ /// <summary>
+ /// Whether to enable sync transcoding for the user.
+ /// </summary>
+ EnableSyncTranscoding = 12,
+
+ /// <summary>
+ /// Whether the user can do media conversion.
+ /// </summary>
+ EnableMediaConversion = 13,
+
+ /// <summary>
+ /// Whether the user has access to all devices.
+ /// </summary>
+ EnableAllDevices = 14,
+
+ /// <summary>
+ /// Whether the user has access to all channels.
+ /// </summary>
+ EnableAllChannels = 15,
+
+ /// <summary>
+ /// Whether the user has access to all folders.
+ /// </summary>
+ EnableAllFolders = 16,
+
+ /// <summary>
+ /// Whether to enable public sharing for the user.
+ /// </summary>
+ EnablePublicSharing = 17,
+
+ /// <summary>
+ /// Whether the user can remotely control other users.
+ /// </summary>
+ EnableRemoteControlOfOtherUsers = 18,
+
+ /// <summary>
+ /// Whether the user is permitted to do playback remuxing.
+ /// </summary>
+ EnablePlaybackRemuxing = 19,
+
+ /// <summary>
+ /// Whether the server should force transcoding on remote connections for the user.
+ /// </summary>
+ ForceRemoteSourceTranscoding = 20
}
}
diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs
index cd2cb791a..de8eecc73 100644
--- a/Jellyfin.Data/Enums/PreferenceKind.cs
+++ b/Jellyfin.Data/Enums/PreferenceKind.cs
@@ -1,13 +1,68 @@
namespace Jellyfin.Data.Enums
{
+ /// <summary>
+ /// The types of user preferences.
+ /// </summary>
public enum PreferenceKind
{
- MaxParentalRating,
- BlockedTags,
- RemoteClientBitrateLimit,
- EnabledDevices,
- EnabledChannels,
- EnabledFolders,
- EnableContentDeletionFromFolders
+ /// <summary>
+ /// A list of blocked tags.
+ /// </summary>
+ BlockedTags = 0,
+
+ /// <summary>
+ /// A list of blocked channels.
+ /// </summary>
+ BlockedChannels = 1,
+
+ /// <summary>
+ /// A list of blocked media folders.
+ /// </summary>
+ BlockedMediaFolders = 2,
+
+ /// <summary>
+ /// A list of enabled devices.
+ /// </summary>
+ EnabledDevices = 3,
+
+ /// <summary>
+ /// A list of enabled channels
+ /// </summary>
+ EnabledChannels = 4,
+
+ /// <summary>
+ /// A list of enabled folders.
+ /// </summary>
+ EnabledFolders = 5,
+
+ /// <summary>
+ /// A list of folders to allow content deletion from.
+ /// </summary>
+ EnableContentDeletionFromFolders = 6,
+
+ /// <summary>
+ /// A list of latest items to exclude.
+ /// </summary>
+ LatestItemExcludes = 7,
+
+ /// <summary>
+ /// A list of media to exclude.
+ /// </summary>
+ MyMediaExcludes = 8,
+
+ /// <summary>
+ /// A list of grouped folders.
+ /// </summary>
+ GroupedFolders = 9,
+
+ /// <summary>
+ /// A list of unrated items to block.
+ /// </summary>
+ BlockUnratedItems = 10,
+
+ /// <summary>
+ /// A list of ordered views.
+ /// </summary>
+ OrderedViews = 11
}
}
diff --git a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs b/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs
index f0aa2b98c..c8fc21159 100644
--- a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs
+++ b/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs
@@ -1,6 +1,6 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
{
public enum SubtitlePlaybackMode
{
diff --git a/MediaBrowser.Model/Configuration/SyncplayAccess.cs b/Jellyfin.Data/Enums/SyncPlayAccess.cs
index d891a8167..8c13b37a1 100644
--- a/MediaBrowser.Model/Configuration/SyncplayAccess.cs
+++ b/Jellyfin.Data/Enums/SyncPlayAccess.cs
@@ -1,4 +1,4 @@
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
{
/// <summary>
/// Enum SyncPlayAccess.
@@ -8,16 +8,16 @@ namespace MediaBrowser.Model.Configuration
/// <summary>
/// User can create groups and join them.
/// </summary>
- CreateAndJoinGroups,
+ CreateAndJoinGroups = 0,
/// <summary>
/// User can only join already existing groups.
/// </summary>
- JoinGroups,
+ JoinGroups = 1,
/// <summary>
/// SyncPlay is disabled for the user.
/// </summary>
- None
+ None = 2
}
}
diff --git a/MediaBrowser.Model/Configuration/UnratedItem.cs b/Jellyfin.Data/Enums/UnratedItem.cs
index e1d1a363d..5259e7739 100644
--- a/MediaBrowser.Model/Configuration/UnratedItem.cs
+++ b/Jellyfin.Data/Enums/UnratedItem.cs
@@ -1,6 +1,6 @@
#pragma warning disable CS1591
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
{
public enum UnratedItem
{
diff --git a/Jellyfin.Data/Enums/Weekday.cs b/Jellyfin.Data/Enums/Weekday.cs
deleted file mode 100644
index b80a03a33..000000000
--- a/Jellyfin.Data/Enums/Weekday.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Jellyfin.Data.Enums
-{
- public enum Weekday
- {
- Sunday,
- Monday,
- Tuesday,
- Wednesday,
- Thursday,
- Friday,
- Saturday
- }
-}
diff --git a/Jellyfin.Data/IHasPermissions.cs b/Jellyfin.Data/IHasPermissions.cs
new file mode 100644
index 000000000..3be72259a
--- /dev/null
+++ b/Jellyfin.Data/IHasPermissions.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data
+{
+ /// <summary>
+ /// An abstraction representing an entity that has permissions.
+ /// </summary>
+ public interface IHasPermissions
+ {
+ /// <summary>
+ /// Gets a collection containing this entity's permissions.
+ /// </summary>
+ ICollection<Permission> Permissions { get; }
+
+ /// <summary>
+ /// Checks whether this entity has the specified permission kind.
+ /// </summary>
+ /// <param name="kind">The kind of permission.</param>
+ /// <returns><c>true</c> if this entity has the specified permission, <c>false</c> otherwise.</returns>
+ bool HasPermission(PermissionKind kind);
+
+ /// <summary>
+ /// Sets the specified permission to the provided value.
+ /// </summary>
+ /// <param name="kind">The kind of permission.</param>
+ /// <param name="value">The value to set.</param>
+ void SetPermission(PermissionKind kind, bool value);
+ }
+}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 58d1ba2f3..282ea511c 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -21,6 +21,7 @@
<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" />
</ItemGroup>
</Project>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index fd533f8ff..dcac1b34b 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -21,8 +21,6 @@
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
- <Compile Remove="Migrations\20200430214405_InitialSchema.cs" />
- <Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index ec09a619f..f574ebc66 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -1,9 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1201 // Constuctors should not follow properties
-#pragma warning disable SA1516 // Elements should be followed by a blank line
-#pragma warning disable SA1623 // Property's documentation should begin with gets or sets
-#pragma warning disable SA1629 // Documentation should end with a period
-#pragma warning disable SA1648 // Inheritdoc should be used with inheriting class
using System.Linq;
using Jellyfin.Data;
@@ -15,7 +10,30 @@ namespace Jellyfin.Server.Implementations
/// <inheritdoc/>
public partial class JellyfinDb : DbContext
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JellyfinDb"/> class.
+ /// </summary>
+ /// <param name="options">The database context options.</param>
+ public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the default connection string.
+ /// </summary>
+ public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
+
+ public virtual DbSet<AccessSchedule> AccessSchedules { get; set; }
+
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
+
+ public virtual DbSet<ImageInfo> ImageInfos { get; set; }
+
+ public virtual DbSet<Permission> Permissions { get; set; }
+
+ public virtual DbSet<Preference> Preferences { get; set; }
+
+ public virtual DbSet<User> Users { get; set; }
/*public virtual DbSet<Artwork> Artwork { get; set; }
public virtual DbSet<Book> Books { get; set; }
public virtual DbSet<BookMetadata> BookMetadata { get; set; }
@@ -42,12 +60,10 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<MovieMetadata> MovieMetadata { get; set; }
public virtual DbSet<MusicAlbum> MusicAlbums { get; set; }
public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; }
- public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Person> People { get; set; }
public virtual DbSet<PersonRole> PersonRoles { get; set; }
public virtual DbSet<Photo> Photo { get; set; }
public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; }
- public virtual DbSet<Preference> Preferences { get; set; }
public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
public virtual DbSet<Rating> Ratings { get; set; }
@@ -62,20 +78,21 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<Series> Series { get; set; }
public virtual DbSet<SeriesMetadata> SeriesMetadata { get; set; }
public virtual DbSet<Track> Tracks { get; set; }
- public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }
- public virtual DbSet<User> Users { get; set; } */
-
- /// <summary>
- /// Gets or sets the default connection string.
- /// </summary>
- public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
+ public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }*/
- /// <inheritdoc />
- public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
+ /// <inheritdoc/>
+ public override int SaveChanges()
{
- }
+ foreach (var saveEntity in ChangeTracker.Entries()
+ .Where(e => e.State == EntityState.Modified)
+ .Select(entry => entry.Entity)
+ .OfType<ISavingChanges>())
+ {
+ saveEntity.OnSavingChanges();
+ }
- partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
+ return base.SaveChanges();
+ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
@@ -83,9 +100,6 @@ namespace Jellyfin.Server.Implementations
CustomInit(optionsBuilder);
}
- partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
- partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
-
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -105,16 +119,10 @@ namespace Jellyfin.Server.Implementations
OnModelCreatedImpl(modelBuilder);
}
- public override int SaveChanges()
- {
- foreach (var saveEntity in ChangeTracker.Entries()
- .Where(e => e.State == EntityState.Modified)
- .OfType<ISavingChanges>())
- {
- saveEntity.OnSavingChanges();
- }
+ partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
- return base.SaveChanges();
- }
+ partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
+
+ partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
}
}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
index eab531d38..8f5c19900 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations
public JellyfinDbProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
- serviceProvider.GetService<JellyfinDb>().Database.Migrate();
+ serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
}
/// <summary>
diff --git a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
new file mode 100644
index 000000000..6342ce9cf
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
@@ -0,0 +1,312 @@
+#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("20200613202153_AddUsers")]
+ partial class AddUsers
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "3.1.4");
+
+ 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.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.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.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+ });
+
+ 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/20200613202153_AddUsers.cs b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.cs
new file mode 100644
index 000000000..7e5a76850
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.cs
@@ -0,0 +1,197 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddUsers : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Users",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(nullable: false),
+ Username = table.Column<string>(maxLength: 255, nullable: false),
+ Password = table.Column<string>(maxLength: 65535, nullable: true),
+ EasyPassword = table.Column<string>(maxLength: 65535, nullable: true),
+ MustUpdatePassword = table.Column<bool>(nullable: false),
+ AudioLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
+ AuthenticationProviderId = table.Column<string>(maxLength: 255, nullable: false),
+ PasswordResetProviderId = table.Column<string>(maxLength: 255, nullable: false),
+ InvalidLoginAttemptCount = table.Column<int>(nullable: false),
+ LastActivityDate = table.Column<DateTime>(nullable: true),
+ LastLoginDate = table.Column<DateTime>(nullable: true),
+ LoginAttemptsBeforeLockout = table.Column<int>(nullable: true),
+ SubtitleMode = table.Column<int>(nullable: false),
+ PlayDefaultAudioTrack = table.Column<bool>(nullable: false),
+ SubtitleLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
+ DisplayMissingEpisodes = table.Column<bool>(nullable: false),
+ DisplayCollectionsView = table.Column<bool>(nullable: false),
+ EnableLocalPassword = table.Column<bool>(nullable: false),
+ HidePlayedInLatest = table.Column<bool>(nullable: false),
+ RememberAudioSelections = table.Column<bool>(nullable: false),
+ RememberSubtitleSelections = table.Column<bool>(nullable: false),
+ EnableNextEpisodeAutoPlay = table.Column<bool>(nullable: false),
+ EnableAutoLogin = table.Column<bool>(nullable: false),
+ EnableUserPreferenceAccess = table.Column<bool>(nullable: false),
+ MaxParentalAgeRating = table.Column<int>(nullable: true),
+ RemoteClientBitrateLimit = table.Column<int>(nullable: true),
+ InternalId = table.Column<long>(nullable: false),
+ SyncPlayAccess = table.Column<int>(nullable: false),
+ RowVersion = table.Column<uint>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AccessSchedules",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ UserId = table.Column<Guid>(nullable: false),
+ DayOfWeek = table.Column<int>(nullable: false),
+ StartHour = table.Column<double>(nullable: false),
+ EndHour = table.Column<double>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AccessSchedules", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AccessSchedules_Users_UserId",
+ column: x => x.UserId,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ImageInfos",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ UserId = table.Column<Guid>(nullable: true),
+ Path = table.Column<string>(maxLength: 512, nullable: false),
+ LastModified = table.Column<DateTime>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ImageInfos", x => x.Id);
+ table.ForeignKey(
+ name: "FK_ImageInfos_Users_UserId",
+ column: x => x.UserId,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Permissions",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Kind = table.Column<int>(nullable: false),
+ Value = table.Column<bool>(nullable: false),
+ RowVersion = table.Column<uint>(nullable: false),
+ Permission_Permissions_Guid = table.Column<Guid>(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Permissions", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Permissions_Users_Permission_Permissions_Guid",
+ column: x => x.Permission_Permissions_Guid,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Preferences",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Kind = table.Column<int>(nullable: false),
+ Value = table.Column<string>(maxLength: 65535, nullable: false),
+ RowVersion = table.Column<uint>(nullable: false),
+ Preference_Preferences_Guid = table.Column<Guid>(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Preferences", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Preferences_Users_Preference_Preferences_Guid",
+ column: x => x.Preference_Preferences_Guid,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AccessSchedules_UserId",
+ schema: "jellyfin",
+ table: "AccessSchedules",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ImageInfos_UserId",
+ schema: "jellyfin",
+ table: "ImageInfos",
+ column: "UserId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Permissions_Permission_Permissions_Guid",
+ schema: "jellyfin",
+ table: "Permissions",
+ column: "Permission_Permissions_Guid");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Preferences_Preference_Preferences_Guid",
+ schema: "jellyfin",
+ table: "Preferences",
+ column: "Preference_Preferences_Guid");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AccessSchedules",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "ImageInfos",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "Permissions",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "Preferences",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "Users",
+ schema: "jellyfin");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 1e7ffd235..51fad8224 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -1,7 +1,9 @@
// <auto-generated />
using System;
+using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
@@ -13,7 +15,32 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "3.1.3");
+ .HasAnnotation("ProductVersion", "3.1.4");
+
+ 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 =>
{
@@ -60,6 +87,221 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ActivityLogs");
});
+
+ 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.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.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+ });
+
+ 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/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
index 02f150607..162dc6f5e 100644
--- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
@@ -1,14 +1,16 @@
+#nullable enable
+
using System;
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.Controller.Entities;
using MediaBrowser.Model.Cryptography;
-namespace Emby.Server.Implementations.Library
+namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// The default authentication provider.
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Library
{
if (resolvedUser == null)
{
- throw new AuthenticationException($"Specified user does not exist.");
+ throw new AuthenticationException("Specified user does not exist.");
}
bool success = false;
@@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.Library
});
}
- byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
+ byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
@@ -69,7 +71,7 @@ namespace Emby.Server.Implementations.Library
{
byte[] calculatedHash = _cryptographyProvider.ComputeHash(
readyHash.Id,
- passwordbytes,
+ passwordBytes,
readyHash.Salt.ToArray());
if (readyHash.Hash.SequenceEqual(calculatedHash))
@@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public bool HasPassword(User user)
- => !string.IsNullOrEmpty(user.Password);
+ => !string.IsNullOrEmpty(user?.Password);
/// <inheritdoc />
public Task ChangePassword(User user, string newPassword)
@@ -129,7 +131,7 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public string GetEasyPasswordHash(User user)
+ public string? GetEasyPasswordHash(User user)
{
return string.IsNullOrEmpty(user.EasyPassword)
? null
diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 6c6fbd86f..cf5a01f08 100644
--- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -1,8 +1,11 @@
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
@@ -10,7 +13,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
-namespace Emby.Server.Implementations.Library
+namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// The default password reset provider.
@@ -51,54 +54,49 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
- SerializablePasswordReset spr;
- List<string> usersreset = new List<string>();
- foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
+ var usersReset = new List<string>();
+ foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
- using (var str = File.OpenRead(resetfile))
+ SerializablePasswordReset spr;
+ await using (var str = File.OpenRead(resetFile))
{
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
}
- if (spr.ExpirationDate < DateTime.Now)
+ if (spr.ExpirationDate < DateTime.UtcNow)
{
- File.Delete(resetfile);
+ File.Delete(resetFile);
}
else if (string.Equals(
spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
pin.Replace("-", string.Empty, StringComparison.Ordinal),
StringComparison.InvariantCultureIgnoreCase))
{
- var resetUser = _userManager.GetUserByName(spr.UserName);
- if (resetUser == null)
- {
- throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
- }
+ 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);
- usersreset.Add(resetUser.Name);
- File.Delete(resetfile);
+ usersReset.Add(resetUser.Username);
+ File.Delete(resetFile);
}
}
- if (usersreset.Count < 1)
+ if (usersReset.Count < 1)
{
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
}
- else
+
+ return new PinRedeemResult
{
- return new PinRedeemResult
- {
- Success = true,
- UsersReset = usersreset.ToArray()
- };
- }
+ Success = true,
+ UsersReset = usersReset.ToArray()
+ };
}
/// <inheritdoc />
- public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
+ public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
{
- string pin = string.Empty;
+ string pin;
using (var cryptoRandom = RandomNumberGenerator.Create())
{
byte[] bytes = new byte[4];
@@ -106,30 +104,33 @@ namespace Emby.Server.Implementations.Library
pin = BitConverter.ToString(bytes);
}
- DateTime expireTime = DateTime.Now.AddMinutes(30);
- string filePath = _passwordResetFileBase + user.InternalId + ".json";
+ DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
+ string filePath = _passwordResetFileBase + user.Id + ".json";
SerializablePasswordReset spr = new SerializablePasswordReset
{
ExpirationDate = expireTime,
Pin = pin,
PinFile = filePath,
- UserName = user.Name
+ UserName = user.Username
};
- using (FileStream fileStream = File.OpenWrite(filePath))
+ await using (FileStream fileStream = File.OpenWrite(filePath))
{
_jsonSerializer.SerializeToStream(spr, fileStream);
await fileStream.FlushAsync().ConfigureAwait(false);
}
+ user.EasyPassword = pin;
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime,
- PinFile = filePath
};
}
+#nullable disable
private class SerializablePasswordReset : PasswordPinCreationResult
{
public string Pin { get; set; }
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
new file mode 100644
index 000000000..140853e52
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
@@ -0,0 +1,67 @@
+#nullable enable
+#pragma warning disable CS1591
+
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Events;
+
+namespace Jellyfin.Server.Implementations.Users
+{
+ public sealed class DeviceAccessEntryPoint : IServerEntryPoint
+ {
+ private readonly IUserManager _userManager;
+ private readonly IAuthenticationRepository _authRepo;
+ private readonly IDeviceManager _deviceManager;
+ private readonly ISessionManager _sessionManager;
+
+ public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
+ {
+ _userManager = userManager;
+ _authRepo = authRepo;
+ _deviceManager = deviceManager;
+ _sessionManager = sessionManager;
+ }
+
+ public Task RunAsync()
+ {
+ _userManager.OnUserUpdated += OnUserUpdated;
+
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private void OnUserUpdated(object? sender, GenericEventArgs<User> e)
+ {
+ var user = e.Argument;
+ if (!user.HasPermission(PermissionKind.EnableAllDevices))
+ {
+ UpdateDeviceAccess(user);
+ }
+ }
+
+ private void UpdateDeviceAccess(User user)
+ {
+ var existing = _authRepo.Get(new AuthenticationInfoQuery
+ {
+ UserId = user.Id
+ }).Items;
+
+ foreach (var authInfo in existing)
+ {
+ if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
+ {
+ _sessionManager.Logout(authInfo);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
index dc61aacd7..491aba1d4 100644
--- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
@@ -1,8 +1,10 @@
+#nullable enable
+
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Entities;
-namespace Emby.Server.Implementations.Library
+namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// An invalid authentication provider.
@@ -40,12 +42,6 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public string GetPasswordHash(User user)
- {
- return string.Empty;
- }
-
- /// <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
new file mode 100644
index 000000000..904114758
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -0,0 +1,841 @@
+#nullable enable
+#pragma warning disable CA1307
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Cryptography;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Users;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Users
+{
+ /// <summary>
+ /// Manages the creation and retrieval of <see cref="User"/> instances.
+ /// </summary>
+ public class UserManager : IUserManager
+ {
+ private readonly JellyfinDbProvider _dbProvider;
+ private readonly ICryptoProvider _cryptoProvider;
+ private readonly INetworkManager _networkManager;
+ 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!;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The database provider.</param>
+ /// <param name="cryptoProvider">The cryptography provider.</param>
+ /// <param name="networkManager">The network manager.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="imageProcessor">The image processor.</param>
+ /// <param name="logger">The logger.</param>
+ public UserManager(
+ JellyfinDbProvider dbProvider,
+ ICryptoProvider cryptoProvider,
+ INetworkManager networkManager,
+ IApplicationHost appHost,
+ IImageProcessor imageProcessor,
+ ILogger<UserManager> logger)
+ {
+ _dbProvider = dbProvider;
+ _cryptoProvider = cryptoProvider;
+ _networkManager = networkManager;
+ _appHost = appHost;
+ _imageProcessor = imageProcessor;
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<User>>? OnUserPasswordChanged;
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<User>>? OnUserCreated;
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<User>>? OnUserDeleted;
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
+
+ /// <inheritdoc/>
+ public IEnumerable<User> Users => _dbProvider.CreateContext().Users;
+
+ /// <inheritdoc/>
+ public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
+
+ /// <inheritdoc/>
+ public User? GetUserById(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentException("Guid can't be empty", nameof(id));
+ }
+
+ return _dbProvider.CreateContext().Users.Find(id);
+ }
+
+ /// <inheritdoc/>
+ public User? GetUserByName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ 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));
+ }
+
+ /// <inheritdoc/>
+ public async Task RenameUser(User user, string newName)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ if (string.IsNullOrWhiteSpace(newName))
+ {
+ throw new ArgumentException("Invalid username", nameof(newName));
+ }
+
+ if (user.Username.Equals(newName, StringComparison.Ordinal))
+ {
+ 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)))
+ {
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "A user with the name '{0}' already exists.",
+ newName));
+ }
+
+ user.Username = newName;
+ await UpdateUserAsync(user).ConfigureAwait(false);
+
+ OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
+ }
+
+ /// <inheritdoc/>
+ public void UpdateUser(User user)
+ {
+ var dbContext = _dbProvider.CreateContext();
+ dbContext.Users.Update(user);
+ dbContext.SaveChanges();
+ }
+
+ /// <inheritdoc/>
+ public async Task UpdateUserAsync(User user)
+ {
+ var dbContext = _dbProvider.CreateContext();
+ dbContext.Users.Update(user);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc/>
+ public User CreateUser(string name)
+ {
+ 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 newUser = new User(
+ name,
+ _defaultAuthenticationProvider.GetType().FullName,
+ _defaultPasswordResetProvider.GetType().FullName)
+ {
+ InternalId = max + 1
+ };
+ dbContext.Users.Add(newUser);
+ dbContext.SaveChanges();
+
+ OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
+
+ return newUser;
+ }
+
+ /// <inheritdoc/>
+ public void DeleteUser(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ var dbContext = _dbProvider.CreateContext();
+
+ if (dbContext.Users.Find(user.Id) == null)
+ {
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
+ user.Username,
+ user.Id));
+ }
+
+ if (dbContext.Users.Count() == 1)
+ {
+ throw new InvalidOperationException(string.Format(
+ CultureInfo.InvariantCulture,
+ "The user '{0}' cannot be deleted because there must be at least one user in the system.",
+ user.Username));
+ }
+
+ if (user.HasPermission(PermissionKind.IsAdministrator)
+ && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
+ {
+ throw new ArgumentException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
+ user.Username),
+ nameof(user));
+ }
+
+ dbContext.Users.Remove(user);
+ dbContext.SaveChanges();
+ OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
+ }
+
+ /// <inheritdoc/>
+ public Task ResetPassword(User user)
+ {
+ return ChangePassword(user, string.Empty);
+ }
+
+ /// <inheritdoc/>
+ public void ResetEasyPassword(User user)
+ {
+ ChangeEasyPassword(user, string.Empty, null);
+ }
+
+ /// <inheritdoc/>
+ public async Task ChangePassword(User user, string newPassword)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
+ await UpdateUserAsync(user).ConfigureAwait(false);
+
+ OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+ }
+
+ /// <inheritdoc/>
+ public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
+ {
+ GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1);
+ UpdateUser(user);
+
+ OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+ }
+
+ /// <inheritdoc/>
+ public UserDto GetUserDto(User user, string? remoteEndPoint = null)
+ {
+ var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
+ return new UserDto
+ {
+ Name = user.Username,
+ Id = user.Id,
+ ServerId = _appHost.SystemId,
+ HasPassword = hasPassword,
+ HasConfiguredPassword = hasPassword,
+ HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword),
+ EnableAutoLogin = user.EnableAutoLogin,
+ LastLoginDate = user.LastLoginDate,
+ LastActivityDate = user.LastActivityDate,
+ PrimaryImageTag = user.ProfileImage != null ? _imageProcessor.GetImageCacheTag(user) : null,
+ Configuration = new UserConfiguration
+ {
+ SubtitleMode = user.SubtitleMode,
+ HidePlayedInLatest = user.HidePlayedInLatest,
+ EnableLocalPassword = user.EnableLocalPassword,
+ PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
+ DisplayCollectionsView = user.DisplayCollectionsView,
+ DisplayMissingEpisodes = user.DisplayMissingEpisodes,
+ AudioLanguagePreference = user.AudioLanguagePreference,
+ RememberAudioSelections = user.RememberAudioSelections,
+ EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
+ RememberSubtitleSelections = user.RememberSubtitleSelections,
+ SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
+ OrderedViews = user.GetPreference(PreferenceKind.OrderedViews),
+ GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders),
+ MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes),
+ LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes)
+ },
+ Policy = new UserPolicy
+ {
+ MaxParentalRating = user.MaxParentalAgeRating,
+ EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
+ RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
+ AuthenticationProviderId = user.AuthenticationProviderId,
+ PasswordResetProviderId = user.PasswordResetProviderId,
+ InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
+ LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
+ IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
+ IsHidden = user.HasPermission(PermissionKind.IsHidden),
+ IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
+ EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
+ EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
+ EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
+ EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
+ EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
+ EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
+ EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+ EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
+ EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
+ EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
+ EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
+ EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
+ EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
+ EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
+ EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers),
+ EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+ ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
+ EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
+ AccessSchedules = user.AccessSchedules.ToArray(),
+ BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
+ EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels),
+ EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
+ EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders),
+ EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
+ SyncPlayAccess = user.SyncPlayAccess,
+ BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels),
+ BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders),
+ BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray()
+ }
+ };
+ }
+
+ /// <inheritdoc/>
+ public async Task<User?> AuthenticateUser(
+ string username,
+ string password,
+ string passwordSha1,
+ string remoteEndPoint,
+ bool isUserSession)
+ {
+ if (string.IsNullOrWhiteSpace(username))
+ {
+ _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
+ throw new ArgumentNullException(nameof(username));
+ }
+
+ var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+ bool success;
+ IAuthenticationProvider? authenticationProvider;
+
+ if (user != null)
+ {
+ var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
+ .ConfigureAwait(false);
+ authenticationProvider = authResult.authenticationProvider;
+ success = authResult.success;
+ }
+ else
+ {
+ var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint)
+ .ConfigureAwait(false);
+ authenticationProvider = authResult.authenticationProvider;
+ string updatedUsername = authResult.username;
+ success = authResult.success;
+
+ if (success
+ && authenticationProvider != null
+ && !(authenticationProvider is DefaultAuthenticationProvider))
+ {
+ // Trust the username returned by the authentication provider
+ username = updatedUsername;
+
+ // 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));
+
+ if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
+ {
+ UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy());
+
+ await UpdateUserAsync(user).ConfigureAwait(false);
+ }
+ }
+ }
+
+ if (success && user != null && authenticationProvider != null)
+ {
+ var providerId = authenticationProvider.GetType().FullName;
+
+ if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
+ {
+ user.AuthenticationProviderId = providerId;
+ await UpdateUserAsync(user).ConfigureAwait(false);
+ }
+ }
+
+ if (user == null)
+ {
+ _logger.LogInformation(
+ "Authentication request for {UserName} has been denied (IP: {IP}).",
+ username,
+ remoteEndPoint);
+ throw new AuthenticationException("Invalid username or password entered.");
+ }
+
+ if (user.HasPermission(PermissionKind.IsDisabled))
+ {
+ _logger.LogInformation(
+ "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).",
+ username,
+ remoteEndPoint);
+ throw new SecurityException(
+ $"The {user.Username} account is currently disabled. Please consult with your administrator.");
+ }
+
+ if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
+ !_networkManager.IsInLocalNetwork(remoteEndPoint))
+ {
+ _logger.LogInformation(
+ "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).",
+ username,
+ remoteEndPoint);
+ throw new SecurityException("Forbidden.");
+ }
+
+ if (!user.IsParentalScheduleAllowed())
+ {
+ _logger.LogInformation(
+ "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).",
+ username,
+ remoteEndPoint);
+ throw new SecurityException("User is not allowed access at this time.");
+ }
+
+ // Update LastActivityDate and LastLoginDate, then save
+ if (success)
+ {
+ if (isUserSession)
+ {
+ user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
+ }
+
+ user.InvalidLoginAttemptCount = 0;
+ await UpdateUserAsync(user).ConfigureAwait(false);
+ _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
+ }
+ else
+ {
+ IncrementInvalidLoginAttemptCount(user);
+ _logger.LogInformation(
+ "Authentication request for {UserName} has been denied (IP: {IP}).",
+ user.Username,
+ remoteEndPoint);
+ }
+
+ return success ? user : null;
+ }
+
+ /// <inheritdoc/>
+ public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
+ {
+ var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
+
+ if (user != null && isInNetwork)
+ {
+ var passwordResetProvider = GetPasswordResetProvider(user);
+ return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
+ }
+
+ return new ForgotPasswordResult
+ {
+ Action = ForgotPasswordAction.InNetworkRequired,
+ PinFile = string.Empty
+ };
+ }
+
+ /// <inheritdoc/>
+ public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
+ {
+ foreach (var provider in _passwordResetProviders)
+ {
+ var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
+
+ if (result.Success)
+ {
+ return result;
+ }
+ }
+
+ return new PinRedeemResult
+ {
+ Success = false,
+ UsersReset = Array.Empty<string>()
+ };
+ }
+
+ /// <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()
+ {
+ // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
+ var dbContext = _dbProvider.CreateContext();
+
+ if (dbContext.Users.Any())
+ {
+ return;
+ }
+
+ var defaultName = Environment.UserName;
+ if (string.IsNullOrWhiteSpace(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);
+ newUser.SetPermission(PermissionKind.IsAdministrator, true);
+ newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
+ newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
+
+ dbContext.Users.Update(newUser);
+ dbContext.SaveChanges();
+ }
+
+ /// <inheritdoc/>
+ public NameIdPair[] GetAuthenticationProviders()
+ {
+ return _authenticationProviders
+ .Where(provider => provider.IsEnabled)
+ .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
+ .ThenBy(i => i.Name)
+ .Select(i => new NameIdPair
+ {
+ Name = i.Name,
+ Id = i.GetType().FullName
+ })
+ .ToArray();
+ }
+
+ /// <inheritdoc/>
+ public NameIdPair[] GetPasswordResetProviders()
+ {
+ return _passwordResetProviders
+ .Where(provider => provider.IsEnabled)
+ .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
+ .ThenBy(i => i.Name)
+ .Select(i => new NameIdPair
+ {
+ Name = i.Name,
+ Id = i.GetType().FullName
+ })
+ .ToArray();
+ }
+
+ /// <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!");
+ user.SubtitleMode = config.SubtitleMode;
+ user.HidePlayedInLatest = config.HidePlayedInLatest;
+ user.EnableLocalPassword = config.EnableLocalPassword;
+ user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
+ user.DisplayCollectionsView = config.DisplayCollectionsView;
+ user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
+ user.AudioLanguagePreference = config.AudioLanguagePreference;
+ user.RememberAudioSelections = config.RememberAudioSelections;
+ user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
+ user.RememberSubtitleSelections = config.RememberSubtitleSelections;
+ user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+
+ user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+ user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+ user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+ user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+ dbContext.Update(user);
+ dbContext.SaveChanges();
+ }
+
+ /// <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!");
+
+ // 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
+ {
+ -1 => null,
+ 0 => 3,
+ _ => policy.LoginAttemptsBeforeLockout
+ };
+
+ user.MaxParentalAgeRating = policy.MaxParentalRating;
+ user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
+ user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
+ user.AuthenticationProviderId = policy.AuthenticationProviderId;
+ user.PasswordResetProviderId = policy.PasswordResetProviderId;
+ user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
+ user.LoginAttemptsBeforeLockout = maxLoginAttempts;
+ user.SyncPlayAccess = policy.SyncPlayAccess;
+ user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+ user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+ user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+ user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+ user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+ user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+ user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+ user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+ user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+ user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+ user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+ user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+ user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+ user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+ user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+ user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+ user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+ user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+ user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+
+ user.AccessSchedules.Clear();
+ foreach (var policyAccessSchedule in policy.AccessSchedules)
+ {
+ user.AccessSchedules.Add(policyAccessSchedule);
+ }
+
+ // TODO: fix this at some point
+ user.SetPreference(
+ PreferenceKind.BlockUnratedItems,
+ policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
+ user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+ user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+ user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+
+ dbContext.Update(user);
+ dbContext.SaveChanges();
+ }
+
+ /// <inheritdoc/>
+ public void ClearProfileImage(User user)
+ {
+ var dbContext = _dbProvider.CreateContext();
+ dbContext.Remove(user.ProfileImage);
+ dbContext.SaveChanges();
+ }
+
+ private static bool IsValidUsername(string name)
+ {
+ // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
+ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
+ // 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\-'._@]*$");
+ }
+
+ private IAuthenticationProvider GetAuthenticationProvider(User user)
+ {
+ return GetAuthenticationProviders(user)[0];
+ }
+
+ private IPasswordResetProvider GetPasswordResetProvider(User user)
+ {
+ return GetPasswordResetProviders(user)[0];
+ }
+
+ private IList<IAuthenticationProvider> GetAuthenticationProviders(User? user)
+ {
+ var authenticationProviderId = user?.AuthenticationProviderId;
+
+ var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
+
+ if (!string.IsNullOrEmpty(authenticationProviderId))
+ {
+ providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+
+ if (providers.Count == 0)
+ {
+ // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
+ _logger.LogWarning(
+ "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected",
+ user?.Username,
+ user?.AuthenticationProviderId);
+ providers = new List<IAuthenticationProvider>
+ {
+ _invalidAuthProvider
+ };
+ }
+
+ return providers;
+ }
+
+ private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
+ {
+ var passwordResetProviderId = user?.PasswordResetProviderId;
+ var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
+
+ if (!string.IsNullOrEmpty(passwordResetProviderId))
+ {
+ providers = providers.Where(i =>
+ string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+ }
+
+ if (providers.Length == 0)
+ {
+ providers = new IPasswordResetProvider[]
+ {
+ _defaultPasswordResetProvider
+ };
+ }
+
+ return providers;
+ }
+
+ private async Task<(IAuthenticationProvider? authenticationProvider, string username, bool success)> AuthenticateLocalUser(
+ string username,
+ string password,
+ User? user,
+ string remoteEndPoint)
+ {
+ bool success = false;
+ IAuthenticationProvider? authenticationProvider = null;
+
+ foreach (var provider in GetAuthenticationProviders(user))
+ {
+ var providerAuthResult =
+ await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
+ var updatedUsername = providerAuthResult.username;
+ success = providerAuthResult.success;
+
+ if (success)
+ {
+ authenticationProvider = provider;
+ username = updatedUsername;
+ break;
+ }
+ }
+
+ if (!success
+ && _networkManager.IsInLocalNetwork(remoteEndPoint)
+ && user?.EnableLocalPassword == true
+ && !string.IsNullOrEmpty(user.EasyPassword))
+ {
+ // Check easy password
+ var passwordHash = PasswordHash.Parse(user.EasyPassword);
+ var hash = _cryptoProvider.ComputeHash(
+ passwordHash.Id,
+ Encoding.UTF8.GetBytes(password),
+ passwordHash.Salt.ToArray());
+ success = passwordHash.Hash.SequenceEqual(hash);
+ }
+
+ return (authenticationProvider, username, success);
+ }
+
+ private async Task<(string username, bool success)> AuthenticateWithProvider(
+ IAuthenticationProvider provider,
+ string username,
+ string password,
+ User? resolvedUser)
+ {
+ try
+ {
+ var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
+ ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
+ : await provider.Authenticate(username, password).ConfigureAwait(false);
+
+ if (authenticationResult.Username != username)
+ {
+ _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
+ username = authenticationResult.Username;
+ }
+
+ return (username, true);
+ }
+ catch (AuthenticationException ex)
+ {
+ _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
+
+ return (username, false);
+ }
+ }
+
+ private void IncrementInvalidLoginAttemptCount(User user)
+ {
+ user.InvalidLoginAttemptCount++;
+ int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
+ if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
+ {
+ user.SetPermission(PermissionKind.IsDisabled, true);
+ OnUserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
+ _logger.LogWarning(
+ "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
+ user.Username,
+ user.InvalidLoginAttemptCount);
+ }
+
+ UpdateUser(user);
+ }
+ }
+}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 331a32c73..fe07411a6 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -7,8 +7,10 @@ using Emby.Server.Implementations;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
+using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
@@ -63,12 +65,15 @@ namespace Jellyfin.Server
// TODO: Set up scoping and use AddDbContextPool
serviceCollection.AddDbContext<JellyfinDb>(
- options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
- ServiceLifetime.Transient);
+ options => options
+ .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")
+ .UseLazyLoadingProxies(),
+ ServiceLifetime.Transient);
serviceCollection.AddSingleton<JellyfinDbProvider>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
+ serviceCollection.AddSingleton<IUserManager, UserManager>();
base.RegisterServices(serviceCollection);
}
@@ -79,7 +84,11 @@ namespace Jellyfin.Server
/// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
+ // Jellyfin.Server
yield return typeof(CoreAppHost).Assembly;
+
+ // Jellyfin.Server.Implementations
+ yield return typeof(JellyfinDb).Assembly;
}
/// <inheritdoc />
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 9cdaa0eb1..aad61d042 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -5,7 +5,10 @@ using System.Linq;
using System.Reflection;
using Jellyfin.Api;
using Jellyfin.Api.Auth;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
@@ -15,6 +18,8 @@ using MediaBrowser.Common.Json;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -33,16 +38,19 @@ namespace Jellyfin.Server.Extensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
+ serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
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.FirstTimeSetupOrElevated,
@@ -51,6 +59,27 @@ namespace Jellyfin.Server.Extensions
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
});
+ options.AddPolicy(
+ Policies.IgnoreSchedule,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new IgnoreScheduleRequirement());
+ });
+ options.AddPolicy(
+ Policies.LocalAccessOnly,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new LocalAccessRequirement());
+ });
+ options.AddPolicy(
+ Policies.RequiresElevation,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new RequiresElevationRequirement());
+ });
});
}
@@ -78,6 +107,10 @@ namespace Jellyfin.Server.Extensions
{
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
})
+ .Configure<ForwardedHeadersOptions>(options =>
+ {
+ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+ })
.AddMvc(opts =>
{
opts.UseGeneralRoutePrefix(baseUrl);
@@ -182,6 +215,31 @@ namespace Jellyfin.Server.Extensions
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/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 473f62737..c98263223 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -19,7 +19,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.DisableTranscodingThrottling),
typeof(Routines.CreateUserLoggingConfigFile),
typeof(Routines.MigrateActivityLogDb),
- typeof(Routines.RemoveDuplicateExtras)
+ typeof(Routines.RemoveDuplicateExtras),
+ typeof(Routines.MigrateUserDb)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
new file mode 100644
index 000000000..2be10c708
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -0,0 +1,208 @@
+using System;
+using System.IO;
+using Emby.Server.Implementations.Data;
+using Emby.Server.Implementations.Serialization;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Users;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+using JsonSerializer = System.Text.Json.JsonSerializer;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// The migration routine for migrating the user database to EF Core.
+ /// </summary>
+ public class MigrateUserDb : IMigrationRoutine
+ {
+ private const string DbFilename = "users.db";
+
+ private readonly ILogger<MigrateUserDb> _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly JellyfinDbProvider _provider;
+ private readonly MyXmlSerializer _xmlSerializer;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="paths">The server application paths.</param>
+ /// <param name="provider">The database provider.</param>
+ /// <param name="xmlSerializer">The xml serializer.</param>
+ public MigrateUserDb(
+ ILogger<MigrateUserDb> logger,
+ IServerApplicationPaths paths,
+ JellyfinDbProvider provider,
+ MyXmlSerializer xmlSerializer)
+ {
+ _logger = logger;
+ _paths = paths;
+ _provider = provider;
+ _xmlSerializer = xmlSerializer;
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
+
+ /// <inheritdoc/>
+ public string Name => "MigrateUserDatabase";
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var dataPath = _paths.DataPath;
+ _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
+
+ using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
+ {
+ var dbContext = _provider.CreateContext();
+
+ var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
+
+ dbContext.RemoveRange(dbContext.Users);
+ dbContext.SaveChanges();
+
+ foreach (var entry in queryResult)
+ {
+ UserMockup mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+ var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
+
+ var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
+ ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"))
+ : new UserConfiguration();
+ var policy = File.Exists(Path.Combine(userDataDir, "policy.xml"))
+ ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"))
+ : new UserPolicy();
+ policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
+ "Emby.Server.Implementations.Library",
+ "Jellyfin.Server.Implementations.Users",
+ StringComparison.Ordinal)
+ ?? typeof(DefaultAuthenticationProvider).FullName;
+
+ policy.PasswordResetProviderId = typeof(DefaultPasswordResetProvider).FullName;
+ int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
+ {
+ -1 => null,
+ 0 => 3,
+ _ => policy.LoginAttemptsBeforeLockout
+ };
+
+ var user = new User(mockup.Name, policy.AuthenticationProviderId, policy.PasswordResetProviderId)
+ {
+ Id = entry[1].ReadGuidFromBlob(),
+ InternalId = entry[0].ToInt64(),
+ MaxParentalAgeRating = policy.MaxParentalRating,
+ EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
+ RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
+ InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
+ LoginAttemptsBeforeLockout = maxLoginAttempts,
+ SubtitleMode = config.SubtitleMode,
+ HidePlayedInLatest = config.HidePlayedInLatest,
+ EnableLocalPassword = config.EnableLocalPassword,
+ PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
+ DisplayCollectionsView = config.DisplayCollectionsView,
+ DisplayMissingEpisodes = config.DisplayMissingEpisodes,
+ AudioLanguagePreference = config.AudioLanguagePreference,
+ RememberAudioSelections = config.RememberAudioSelections,
+ EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
+ RememberSubtitleSelections = config.RememberSubtitleSelections,
+ SubtitleLanguagePreference = config.SubtitleLanguagePreference,
+ Password = mockup.Password,
+ EasyPassword = mockup.EasyPassword,
+ LastLoginDate = mockup.LastLoginDate,
+ LastActivityDate = mockup.LastActivityDate
+ };
+
+ if (mockup.ImageInfos.Length > 0)
+ {
+ ItemImageInfo info = mockup.ImageInfos[0];
+
+ user.ProfileImage = new ImageInfo(info.Path)
+ {
+ LastModified = info.DateModified
+ };
+ }
+
+ user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+ user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+ user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+ user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+ user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+ user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+ user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+ user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+ user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+ user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+ user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+ user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+ user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+ user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+ user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+ user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+ user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+ user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+ user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+
+ foreach (var policyAccessSchedule in policy.AccessSchedules)
+ {
+ user.AccessSchedules.Add(policyAccessSchedule);
+ }
+
+ user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+ user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+ user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+ user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+ user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+ user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+ user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+ dbContext.Users.Add(user);
+ }
+
+ dbContext.SaveChanges();
+ }
+
+ try
+ {
+ File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+
+ var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
+ if (File.Exists(journalPath))
+ {
+ File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
+ }
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
+ }
+ }
+
+#nullable disable
+ internal class UserMockup
+ {
+ public string Password { get; set; }
+
+ public string EasyPassword { get; set; }
+
+ public DateTime? LastLoginDate { get; set; }
+
+ public DateTime? LastActivityDate { get; set; }
+
+ public string Name { get; set; }
+
+ public ItemImageInfo[] ImageInfos { get; set; }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 3971a08e9..dfc7bbbb1 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -14,9 +14,9 @@ using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking;
+using Jellyfin.Api.Controllers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Extensions;
-using MediaBrowser.WebDashboard.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
@@ -172,7 +172,7 @@ namespace Jellyfin.Server
// If hosting the web client, validate the client content path
if (startupConfig.HostWebClient())
{
- string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager);
+ string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager);
if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
{
throw new InvalidOperationException(
diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs
index 2cd68ac1b..2ece16ee1 100644
--- a/MediaBrowser.Api/BaseApiService.cs
+++ b/MediaBrowser.Api/BaseApiService.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -94,8 +95,8 @@ namespace MediaBrowser.Api
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.Policy.IsAdministrator)
- || (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess))
+ if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
+ || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
{
throw new SecurityException("Unauthorized access.");
}
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/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
deleted file mode 100644
index 62c4ff43f..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/FilterService.cs b/MediaBrowser.Api/FilterService.cs
deleted file mode 100644
index 5eb72cdb1..000000000
--- a/MediaBrowser.Api/FilterService.cs
+++ /dev/null
@@ -1,243 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- [Route("/Items/Filters", "GET", Summary = "Gets branding configuration")]
- public class GetQueryFiltersLegacy : IReturn<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/Images/ImageByNameService.cs b/MediaBrowser.Api/Images/ImageByNameService.cs
deleted file mode 100644
index 45b7d0c10..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/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index fdb15d96a..0b8ddeacd 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/MediaBrowser.Api/Images/ImageService.cs
@@ -18,9 +18,11 @@ 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
{
@@ -408,14 +410,14 @@ namespace MediaBrowser.Api.Images
{
var item = _userManager.GetUserById(request.Id);
- return GetImage(request, Guid.Empty, item, false);
+ return GetImage(request, item, false);
}
public object Head(GetUserImage request)
{
var item = _userManager.GetUserById(request.Id);
- return GetImage(request, Guid.Empty, item, true);
+ return GetImage(request, item, true);
}
public object Get(GetItemByNameImage request)
@@ -448,9 +450,9 @@ namespace MediaBrowser.Api.Images
request.Type = Enum.Parse<ImageType>(GetPathValue(3).ToString(), true);
- var item = _userManager.GetUserById(id);
+ var user = _userManager.GetUserById(id);
- return PostImage(item, request.RequestStream, request.Type, Request.ContentType);
+ return PostImage(user, request.RequestStream, Request.ContentType);
}
/// <summary>
@@ -477,9 +479,17 @@ namespace MediaBrowser.Api.Images
var userId = request.Id;
AssertCanUpdateUser(_authContext, _userManager, userId, true);
- var item = _userManager.GetUserById(userId);
+ var user = _userManager.GetUserById(userId);
+ try
+ {
+ File.Delete(user.ProfileImage.Path);
+ }
+ catch (IOException e)
+ {
+ Logger.LogError(e, "Error deleting user profile image:");
+ }
- item.DeleteImage(request.Type, request.Index ?? 0);
+ _userManager.ClearProfileImage(user);
}
/// <summary>
@@ -567,14 +577,14 @@ namespace MediaBrowser.Api.Images
throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
}
- bool cropwhitespace;
+ bool cropWhitespace;
if (request.CropWhitespace.HasValue)
{
- cropwhitespace = request.CropWhitespace.Value;
+ cropWhitespace = request.CropWhitespace.Value;
}
else
{
- cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
+ cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
}
var outputFormats = GetOutputFormats(request);
@@ -597,7 +607,35 @@ namespace MediaBrowser.Api.Images
itemId,
request,
imageInfo,
- cropwhitespace,
+ 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,
@@ -605,6 +643,55 @@ namespace MediaBrowser.Api.Images
}
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,
@@ -741,13 +828,35 @@ namespace MediaBrowser.Api.Images
/// <param name="request">The request.</param>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
- private ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
+ 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>
@@ -758,22 +867,41 @@ namespace MediaBrowser.Api.Images
/// <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);
-
- var memoryStream = new MemoryStream(bytes)
+ 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(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
-
- entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ 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 358ac30fa..000000000
--- a/MediaBrowser.Api/Images/RemoteImageService.cs
+++ /dev/null
@@ -1,297 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
- public class BaseRemoteImageRequest : IReturn<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 68e3dfa59..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
index 9d53a2610..e96875403 100644
--- a/MediaBrowser.Api/Library/LibraryService.cs
+++ b/MediaBrowser.Api/Library/LibraryService.cs
@@ -6,6 +6,7 @@ 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;
@@ -14,7 +15,6 @@ using MediaBrowser.Controller.Dto;
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;
@@ -27,6 +27,12 @@ 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
{
@@ -350,6 +356,7 @@ namespace MediaBrowser.Api.Library
_moviesServiceLogger = moviesServiceLogger;
}
+ // Content Types available for each Library
private string[] GetRepresentativeItemTypes(string contentType)
{
return contentType switch
@@ -359,7 +366,7 @@ namespace MediaBrowser.Api.Library
CollectionType.Movies => new[] {"Movie"},
CollectionType.TvShows => new[] {"Series", "Season", "Episode"},
CollectionType.Books => new[] {"Book"},
- CollectionType.Music => new[] {"MusicAlbum", "MusicArtist", "Audio", "MusicVideo"},
+ CollectionType.Music => new[] {"MusicArtist", "MusicAlbum", "Audio", "MusicVideo"},
CollectionType.HomeVideos => new[] {"Video", "Photo"},
CollectionType.Photos => new[] {"Video", "Photo"},
CollectionType.MusicVideos => new[] {"MusicVideo"},
@@ -425,7 +432,6 @@ namespace MediaBrowser.Api.Library
return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
}
@@ -763,8 +769,8 @@ namespace MediaBrowser.Api.Library
{
try
{
- _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
- string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name),
+ _activityManager.Create(new ActivityLog(
+ string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
"UserDownloadingContent",
auth.UserId)
{
diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs
deleted file mode 100644
index 1e300814f..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
index 5fe4c0cca..279fd6ee9 100644
--- a/MediaBrowser.Api/LiveTv/LiveTvService.cs
+++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs
@@ -7,6 +7,7 @@ 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;
@@ -859,7 +860,7 @@ namespace MediaBrowser.Api.LiveTv
throw new SecurityException("Anonymous live tv management is not allowed.");
}
- if (!user.Policy.EnableLiveTvManagement)
+ if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
{
throw new SecurityException("The current user does not have permission to manage live tv.");
}
diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs
deleted file mode 100644
index 6a69d2656..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/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
index a9c7f5d6f..2d61299c7 100644
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ b/MediaBrowser.Api/Movies/MoviesService.cs
@@ -2,11 +2,11 @@ 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.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
@@ -15,6 +15,8 @@ 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
{
@@ -251,7 +253,12 @@ namespace MediaBrowser.Api.Movies
return categories.OrderBy(i => i.RecommendationType);
}
- private IEnumerable<RecommendationDto> GetWithDirector(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ 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)
diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs
index cacec8d64..7d10c9427 100644
--- a/MediaBrowser.Api/Music/InstantMixService.cs
+++ b/MediaBrowser.Api/Music/InstantMixService.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index 24297d500..957e4ae91 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -7,6 +7,7 @@ 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;
@@ -196,7 +197,7 @@ namespace MediaBrowser.Api.Playback
if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
- if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding)
+ if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index e2d771ec6..2c6534cc0 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -9,6 +9,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -400,21 +401,24 @@ namespace MediaBrowser.Api.Playback
if (item is Audio)
{
- Logger.LogInformation("User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Name, user.Policy.EnableAudioPlaybackTranscoding);
+ Logger.LogInformation(
+ "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
+ user.Username,
+ user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
else
{
Logger.LogInformation("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
- user.Name,
- user.Policy.EnablePlaybackRemuxing,
- user.Policy.EnableVideoPlaybackTranscoding,
- user.Policy.EnableAudioPlaybackTranscoding);
+ user.Username,
+ user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+ user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+ user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
// Beginning of Playback Determination: Attempt DirectPlay first
if (mediaSource.SupportsDirectPlay)
{
- if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
+ if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectPlay = false;
}
@@ -428,14 +432,16 @@ namespace MediaBrowser.Api.Playback
if (item is Audio)
{
- if (!user.Policy.EnableAudioPlaybackTranscoding)
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectPlay = true;
}
}
else if (item is Video)
{
- if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectPlay = true;
}
@@ -463,7 +469,7 @@ namespace MediaBrowser.Api.Playback
if (mediaSource.SupportsDirectStream)
{
- if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
+ if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectStream = false;
}
@@ -473,14 +479,16 @@ namespace MediaBrowser.Api.Playback
if (item is Audio)
{
- if (!user.Policy.EnableAudioPlaybackTranscoding)
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectStream = true;
}
}
else if (item is Video)
{
- if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectStream = true;
}
@@ -512,7 +520,7 @@ namespace MediaBrowser.Api.Playback
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
- if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
+ if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
if (streamInfo != null)
{
@@ -576,10 +584,10 @@ namespace MediaBrowser.Api.Playback
}
}
- private long? GetMaxBitrate(long? clientMaxBitrate, User user)
+ private long? GetMaxBitrate(long? clientMaxBitrate, Jellyfin.Data.Entities.User user)
{
var maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0;
+ var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
if (remoteClientMaxBitrate <= 0)
{
diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs
deleted file mode 100644
index 953b00e35..000000000
--- a/MediaBrowser.Api/PlaylistService.cs
+++ /dev/null
@@ -1,217 +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 7f74511ee..000000000
--- a/MediaBrowser.Api/PluginService.cs
+++ /dev/null
@@ -1,268 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- /// <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/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs
deleted file mode 100644
index 020bb5042..000000000
--- a/MediaBrowser.Api/Sessions/SessionService.cs
+++ /dev/null
@@ -1,498 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Sessions
-{
- /// <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.Policy.EnableRemoteControlOfOtherUsers)
- {
- result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId));
- }
-
- if (!user.Policy.EnableSharedDeviceControl)
- {
- result = result.Where(i => !i.UserId.Equals(Guid.Empty));
- }
-
- if (request.ActiveWithinSeconds.HasValue && request.ActiveWithinSeconds.Value > 0)
- {
- var minActiveDate = DateTime.UtcNow.AddSeconds(0 - request.ActiveWithinSeconds.Value);
- result = result.Where(i => i.LastActivityDate >= minActiveDate);
- }
-
- result = result.Where(i =>
- {
- var deviceId = i.DeviceId;
-
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- if (!_deviceManager.CanAccessDevice(user, deviceId))
- {
- return false;
- }
- }
-
- return true;
- });
- }
-
- return ToOptimizedResult(result.ToArray());
- }
-
- public Task Post(SendPlaystateCommand request)
- {
- return _sessionManager.SendPlaystateCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None);
- }
-
- /// <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 f2968c6b5..000000000
--- a/MediaBrowser.Api/Subtitles/SubtitleService.cs
+++ /dev/null
@@ -1,300 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace MediaBrowser.Api.Subtitles
-{
- [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
- [Authenticated(Roles = "Admin")]
- public class DeleteSubtitle
- {
- /// <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 91f85db6f..000000000
--- a/MediaBrowser.Api/SuggestionsService.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using System;
-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.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/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs
deleted file mode 100644
index c57cc93d5..000000000
--- a/MediaBrowser.Api/System/SystemService.cs
+++ /dev/null
@@ -1,226 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
- /// <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/TestService.cs b/MediaBrowser.Api/TestService.cs
new file mode 100644
index 000000000..6c999e08d
--- /dev/null
+++ b/MediaBrowser.Api/TestService.cs
@@ -0,0 +1,26 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Api
+{
+ /// <summary>
+ /// Service for testing path value.
+ /// </summary>
+ public class TestService : BaseApiService
+ {
+ /// <summary>
+ /// Test service.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{TestService}"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="httpResultFactory">Instance of the <see cref="IHttpResultFactory"/> interface.</param>
+ public TestService(
+ ILogger<TestService> logger,
+ IServerConfigurationManager serverConfigurationManager,
+ IHttpResultFactory httpResultFactory)
+ : base(logger, serverConfigurationManager, httpResultFactory)
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs
deleted file mode 100644
index cd8e8dfbe..000000000
--- a/MediaBrowser.Api/TvShowsService.cs
+++ /dev/null
@@ -1,498 +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, user);
-
- 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, User user)
- {
- 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, user);
-
- 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, user);
-
- 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/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
index 559082ff4..a1ec08467 100644
--- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
+++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
@@ -1,6 +1,7 @@
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;
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
index f3c0441e1..49d534c36 100644
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs
@@ -2,10 +2,11 @@ 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.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
@@ -14,6 +15,7 @@ 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
{
@@ -86,7 +88,7 @@ namespace MediaBrowser.Api.UserLibrary
var ancestorIds = Array.Empty<Guid>();
- var excludeFolderIds = user.Configuration.LatestItemsExcludes;
+ var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
{
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
@@ -211,14 +213,14 @@ namespace MediaBrowser.Api.UserLibrary
request.IncludeItemTypes = "Playlist";
}
- bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id)
+ bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
// Assume all folders inside an EnabledChannel are enabled
- || user.Policy.EnabledChannels.Any(i => new Guid(i) == item.Id);
+ || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
{
- if (user.Policy.EnabledFolders.Contains(
+ if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
StringComparer.OrdinalIgnoreCase))
{
@@ -226,9 +228,12 @@ namespace MediaBrowser.Api.UserLibrary
}
}
- if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder && !user.Policy.EnableAllChannels)
+ if (!(item is UserRootFolder)
+ && !isInEnabledFolder
+ && !user.HasPermission(PermissionKind.EnableAllFolders)
+ && !user.HasPermission(PermissionKind.EnableAllChannels))
{
- Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name);
+ Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
return new QueryResult<BaseItem>
{
Items = Array.Empty<BaseItem>(),
diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs
index d0faca163..ab231626b 100644
--- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs
+++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs
@@ -1,8 +1,8 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
index 7fa750adb..f75852885 100644
--- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
+++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
@@ -312,7 +312,7 @@ namespace MediaBrowser.Api.UserLibrary
if (!request.IsPlayed.HasValue)
{
- if (user.Configuration.HidePlayedInLatest)
+ if (user.HidePlayedInLatest)
{
request.IsPlayed = false;
}
diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs
deleted file mode 100644
index 78fc6c694..000000000
--- a/MediaBrowser.Api/UserService.cs
+++ /dev/null
@@ -1,600 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-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.Policy.IsDisabled == request.IsDisabled.Value);
- }
-
- if (request.IsHidden.HasValue)
- {
- users = users.Where(i => i.Policy.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.Policy.EnableRemoteAccess);
- }
- }
-
- var result = users
- .OrderBy(u => u.Name)
- .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)
- {
- var user = _userManager.GetUserById(request.Id);
-
- if (user == null)
- {
- throw new ResourceNotFoundException("User not found");
- }
-
- _sessionMananger.RevokeUserTokens(user.Id, null);
- _userManager.DeleteUser(user);
- return Task.CompletedTask;
- }
-
- /// <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.Name,
- 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.Name, 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.Name, dtoUser.Name, StringComparison.Ordinal))
- {
- _userManager.UpdateUser(user);
- _userManager.UpdateConfiguration(user, 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.Policy.IsAdministrator)
- {
- if (_userManager.Users.Count(i => i.Policy.IsAdministrator) == 1)
- {
- throw new ArgumentException("There must be at least one user in the system with administrative access.");
- }
- }
-
- // If disabling
- if (request.IsDisabled && user.Policy.IsAdministrator)
- {
- throw new ArgumentException("Administrators cannot be disabled.");
- }
-
- // If disabling
- if (request.IsDisabled && !user.Policy.IsDisabled)
- {
- if (_userManager.Users.Count(i => !i.Policy.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.UpdateUserPolicy(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/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/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index adc15123b..ec3c45476 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -28,6 +28,7 @@ namespace MediaBrowser.Common.Json
};
options.Converters.Add(new JsonGuidConverter());
+ options.Converters.Add(new JsonInt32Converter());
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index f5571065f..c0324a384 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -1,5 +1,5 @@
using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Authentication
diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
index 2639960e7..d9b814f69 100644
--- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Authentication
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index cdf2ca69e..dbb047804 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -3,6 +3,8 @@ using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Querying;
@@ -13,16 +15,18 @@ namespace MediaBrowser.Controller.Channels
{
public override bool IsVisible(User user)
{
- if (user.Policy.BlockedChannels != null)
+ if (user.GetPreference(PreferenceKind.BlockedChannels) != null)
{
- if (user.Policy.BlockedChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
else
{
- if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (!user.HasPermission(PermissionKind.EnableAllChannels)
+ && !user.GetPreference(PreferenceKind.EnabledChannels)
+ .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{
return false;
}
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index cfe8493d3..701423c0f 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index ef3f43c75..7d279230b 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -1,5 +1,5 @@
using System;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index 8800fdf99..f1873d539 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
@@ -57,6 +58,8 @@ namespace MediaBrowser.Controller.Drawing
string GetImageCacheTag(BaseItem item, ChapterInfo info);
+ string GetImageCacheTag(User user);
+
/// <summary>
/// Processes the image.
/// </summary>
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
index d5a5f547e..c87a248b5 100644
--- a/MediaBrowser.Controller/Drawing/ImageHelper.cs
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -57,6 +57,7 @@ namespace MediaBrowser.Controller.Drawing
case ImageType.BoxRear:
case ImageType.Disc:
case ImageType.Menu:
+ case ImageType.Profile:
return 1;
case ImageType.Logo:
return 2.58;
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index ba693a065..56e6c47c4 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index a700d0be4..a8ea2157d 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -2,9 +2,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index b702bdb71..f7b2f9549 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -4,12 +4,13 @@ using System.Linq;
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.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Users;
+using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -114,9 +115,9 @@ namespace MediaBrowser.Controller.Entities.Audio
return list;
}
- protected override bool GetBlockUnratedValue(UserPolicy config)
+ protected override bool GetBlockUnratedValue(User user)
{
- return config.BlockUnratedItems.Contains(UnratedItem.Music);
+ return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 31136e004..63db3cfab 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -4,12 +4,13 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
+using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -76,11 +77,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public override int GetChildCount(User user)
{
- if (IsAccessedByName)
- {
- return 0;
- }
- return base.GetChildCount(user);
+ return IsAccessedByName ? 0 : base.GetChildCount(user);
}
public override bool IsSaveLocalMetadataEnabled()
@@ -142,9 +139,10 @@ namespace MediaBrowser.Controller.Entities.Audio
{
return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
}
- protected override bool GetBlockUnratedValue(UserPolicy config)
+
+ protected override bool GetBlockUnratedValue(User user)
{
- return config.BlockUnratedItems.Contains(UnratedItem.Music);
+ return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs
index a13873bf9..4adaf4c6e 100644
--- a/MediaBrowser.Controller/Entities/AudioBook.cs
+++ b/MediaBrowser.Controller/Entities/AudioBook.cs
@@ -1,7 +1,7 @@
using System;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Controller.Entities
{
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 98c416bb7..0eabb4b7a 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -7,6 +7,8 @@ using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -24,7 +26,6 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities
@@ -481,12 +482,12 @@ namespace MediaBrowser.Controller.Entities
public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
{
- if (user.Policy.EnableContentDeletion)
+ if (user.HasPermission(PermissionKind.EnableContentDeletion))
{
return true;
}
- var allowed = user.Policy.EnableContentDeletionFromFolders;
+ var allowed = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders);
if (SourceType == SourceType.Channel)
{
@@ -527,7 +528,7 @@ namespace MediaBrowser.Controller.Entities
public virtual bool IsAuthorizedToDownload(User user)
{
- return user.Policy.EnableContentDownloading;
+ return user.HasPermission(PermissionKind.EnableContentDownloading);
}
public bool CanDownload(User user)
@@ -1005,7 +1006,7 @@ namespace MediaBrowser.Controller.Entities
/// <returns>PlayAccess.</returns>
public PlayAccess GetPlayAccess(User user)
{
- if (!user.Policy.EnableMediaPlayback)
+ if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
{
return PlayAccess.None;
}
@@ -1214,11 +1215,11 @@ namespace MediaBrowser.Controller.Entities
{
if (video.IsoType.HasValue)
{
- if (video.IsoType.Value == Model.Entities.IsoType.BluRay)
+ if (video.IsoType.Value == IsoType.BluRay)
{
terms.Add("Bluray");
}
- else if (video.IsoType.Value == Model.Entities.IsoType.Dvd)
+ else if (video.IsoType.Value == IsoType.Dvd)
{
terms.Add("DVD");
}
@@ -1774,7 +1775,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- var maxAllowedRating = user.Policy.MaxParentalRating;
+ var maxAllowedRating = user.MaxParentalAgeRating;
if (maxAllowedRating == null)
{
@@ -1790,7 +1791,7 @@ namespace MediaBrowser.Controller.Entities
if (string.IsNullOrEmpty(rating))
{
- return !GetBlockUnratedValue(user.Policy);
+ return !GetBlockUnratedValue(user);
}
var value = LocalizationManager.GetRatingLevel(rating);
@@ -1798,7 +1799,7 @@ namespace MediaBrowser.Controller.Entities
// Could not determine the integer value
if (!value.HasValue)
{
- var isAllowed = !GetBlockUnratedValue(user.Policy);
+ var isAllowed = !GetBlockUnratedValue(user);
if (!isAllowed)
{
@@ -1860,8 +1861,7 @@ namespace MediaBrowser.Controller.Entities
private bool IsVisibleViaTags(User user)
{
- var policy = user.Policy;
- if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
{
return false;
}
@@ -1887,22 +1887,18 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the block unrated value.
/// </summary>
- /// <param name="config">The configuration.</param>
+ /// <param name="user">The configuration.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- protected virtual bool GetBlockUnratedValue(UserPolicy config)
+ protected virtual bool GetBlockUnratedValue(User user)
{
// Don't block plain folders that are unrated. Let the media underneath get blocked
// Special folders like series and albums will override this method.
- if (IsFolder)
- {
- return false;
- }
- if (this is IItemByName)
+ if (IsFolder || this is IItemByName)
{
return false;
}
- return config.BlockUnratedItems.Contains(GetBlockUnratedType());
+ return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType().ToString());
}
/// <summary>
@@ -2132,7 +2128,8 @@ namespace MediaBrowser.Controller.Entities
/// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException"></exception>
- public virtual void MarkPlayed(User user,
+ public virtual void MarkPlayed(
+ User user,
DateTime? datePlayed,
bool resetPosition)
{
@@ -2806,14 +2803,7 @@ namespace MediaBrowser.Controller.Entities
return this;
}
- foreach (var parent in GetParents())
- {
- if (parent.IsTopParent)
- {
- return parent;
- }
- }
- return null;
+ return GetParents().FirstOrDefault(parent => parent.IsTopParent);
}
[JsonIgnore]
diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs
index c5ce001c0..11c6c6e45 100644
--- a/MediaBrowser.Controller/Entities/Book.cs
+++ b/MediaBrowser.Controller/Entities/Book.cs
@@ -1,8 +1,8 @@
using System;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Controller.Entities
{
diff --git a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs
deleted file mode 100644
index 8a79e0783..000000000
--- a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.Configuration;
-
-namespace MediaBrowser.Controller.Entities
-{
- public static class DayOfWeekHelper
- {
- public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
- {
- return GetDaysOfWeek(new List<DynamicDayOfWeek> { day });
- }
-
- public static List<DayOfWeek> GetDaysOfWeek(List<DynamicDayOfWeek> days)
- {
- var list = new List<DayOfWeek>();
-
- if (days.Contains(DynamicDayOfWeek.Sunday) ||
- days.Contains(DynamicDayOfWeek.Weekend) ||
- days.Contains(DynamicDayOfWeek.Everyday))
- {
- list.Add(DayOfWeek.Sunday);
- }
-
- if (days.Contains(DynamicDayOfWeek.Saturday) ||
- days.Contains(DynamicDayOfWeek.Weekend) ||
- days.Contains(DynamicDayOfWeek.Everyday))
- {
- list.Add(DayOfWeek.Saturday);
- }
-
- if (days.Contains(DynamicDayOfWeek.Monday) ||
- days.Contains(DynamicDayOfWeek.Weekday) ||
- days.Contains(DynamicDayOfWeek.Everyday))
- {
- list.Add(DayOfWeek.Monday);
- }
-
- if (days.Contains(DynamicDayOfWeek.Tuesday) ||
- days.Contains(DynamicDayOfWeek.Weekday) ||
- days.Contains(DynamicDayOfWeek.Everyday))
- {
- list.Add(DayOfWeek.Tuesday
- );
- }
-
- if (days.Contains(DynamicDayOfWeek.Wednesday) ||
- days.Contains(DynamicDayOfWeek.Weekday) ||
- days.Contains(DynamicDayOfWeek.Everyday))
- {
- list.Add(DayOfWeek.Wednesday);
- }
-
- if (days.Contains(DynamicDayOfWeek.Thursday) ||
- days.Contains(DynamicDayOfWeek.Weekday) ||
- days.Contains(DynamicDayOfWeek.Everyday))
- {
- list.Add(DayOfWeek.Thursday);
- }
-
- if (days.Contains(DynamicDayOfWeek.Friday) ||
- days.Contains(DynamicDayOfWeek.Weekday) ||
- days.Contains(DynamicDayOfWeek.Everyday))
- {
- list.Add(DayOfWeek.Friday);
- }
-
- return list;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 29040f92e..4af74f9cd 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -8,6 +8,8 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Collections;
@@ -15,13 +17,16 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
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.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Controller.Entities
{
@@ -177,19 +182,22 @@ namespace MediaBrowser.Controller.Entities
{
if (this is ICollectionFolder && !(this is BasePluginFolder))
{
- if (user.Policy.BlockedMediaFolders != null)
+ var blockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders);
+ if (blockedMediaFolders.Length > 0)
{
- if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) ||
+ if (blockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) ||
// Backwards compatibility
- user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+ blockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
else
{
- if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (!user.HasPermission(PermissionKind.EnableAllFolders)
+ && !user.GetPreference(PreferenceKind.EnabledFolders)
+ .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{
return false;
}
@@ -882,7 +890,7 @@ namespace MediaBrowser.Controller.Entities
try
{
query.Parent = this;
- query.ChannelIds = new Guid[] { ChannelId };
+ query.ChannelIds = new[] { ChannelId };
// Don't blow up here because it could cause parent screens with other content to fail
return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result;
@@ -952,11 +960,13 @@ namespace MediaBrowser.Controller.Entities
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
}
- private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items,
+ private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
+ IEnumerable<BaseItem> items,
InternalItemsQuery query,
BaseItem queryParent,
User user,
- IServerConfigurationManager configurationManager, ICollectionManager collectionManager)
+ IServerConfigurationManager configurationManager,
+ ICollectionManager collectionManager)
{
if (items == null)
{
@@ -1582,7 +1592,7 @@ namespace MediaBrowser.Controller.Entities
EnableTotalRecordCount = false
};
- if (!user.Configuration.DisplayMissingEpisodes)
+ if (!user.DisplayMissingEpisodes)
{
query.IsVirtualItem = false;
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index bd96059e3..496bee857 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@@ -223,15 +224,16 @@ namespace MediaBrowser.Controller.Entities
{
if (user != null)
{
- var policy = user.Policy;
- MaxParentalRating = policy.MaxParentalRating;
+ MaxParentalRating = user.MaxParentalAgeRating;
- if (policy.MaxParentalRating.HasValue)
+ if (MaxParentalRating.HasValue)
{
- BlockUnratedItems = policy.BlockUnratedItems.Where(i => i != UnratedItem.Other).ToArray();
+ BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
+ .Where(i => i != UnratedItem.Other.ToString())
+ .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
}
- ExcludeInheritedTags = policy.BlockedTags;
+ ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
User = user;
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index feaf8c45a..be71bcc3c 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Entities.Movies
{
@@ -45,9 +45,9 @@ namespace MediaBrowser.Controller.Entities.Movies
/// <value>The display order.</value>
public string DisplayOrder { get; set; }
- protected override bool GetBlockUnratedValue(UserPolicy config)
+ protected override bool GetBlockUnratedValue(User user)
{
- return config.BlockUnratedItems.Contains(UnratedItem.Movie);
+ return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie.ToString());
}
public override double GetDefaultPrimaryImageAspectRatio()
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index d5c9ff025..26a165025 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -4,8 +4,8 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs
index 603242063..6e7f2812d 100644
--- a/MediaBrowser.Controller/Entities/MusicVideo.cs
+++ b/MediaBrowser.Controller/Entities/MusicVideo.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Controller.Entities
{
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 49229fa4b..4ec60e7cd 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 9c8a469e2..7dfd1a759 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Entities.TV
{
@@ -168,7 +168,7 @@ namespace MediaBrowser.Controller.Entities.TV
return GetEpisodes(user, new DtoOptions(true));
}
- protected override bool GetBlockUnratedValue(UserPolicy config)
+ protected override bool GetBlockUnratedValue(User user)
{
// Don't block. Let either the entire series rating or episode rating determine it
return false;
@@ -203,7 +203,7 @@ namespace MediaBrowser.Controller.Entities.TV
public Guid FindSeriesId()
{
var series = FindParent<Series>();
- return series == null ? Guid.Empty : series.Id;
+ return series?.Id ?? Guid.Empty;
}
/// <summary>
@@ -234,7 +234,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{
- IndexNumber = IndexNumber ?? LibraryManager.GetSeasonNumberFromPath(Path);
+ IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path);
// If a change was made record it
if (IndexNumber.HasValue)
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 6f1c2501f..a519089b3 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -5,13 +5,14 @@ using System.Linq;
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.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Users;
+using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
namespace MediaBrowser.Controller.Entities.TV
{
@@ -119,7 +120,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { typeof(Season).Name },
+ IncludeItemTypes = new[] { nameof(Season) },
IsVirtualItem = false,
Limit = 0,
DtoOptions = new DtoOptions(false)
@@ -205,14 +206,9 @@ namespace MediaBrowser.Controller.Entities.TV
query.IncludeItemTypes = new[] { typeof(Season).Name };
query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
- if (user != null)
+ if (user != null && !user.DisplayMissingEpisodes)
{
- var config = user.Configuration;
-
- if (!config.DisplayMissingEpisodes)
- {
- query.IsMissing = false;
- }
+ query.IsMissing = false;
}
}
@@ -257,8 +253,8 @@ namespace MediaBrowser.Controller.Entities.TV
OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
};
- var config = user.Configuration;
- if (!config.DisplayMissingEpisodes)
+
+ if (!user.DisplayMissingEpisodes)
{
query.IsMissing = false;
}
@@ -311,7 +307,7 @@ namespace MediaBrowser.Controller.Entities.TV
// Refresh episodes and other children
foreach (var item in items)
{
- if ((item is Season))
+ if (item is Season)
{
continue;
}
@@ -370,8 +366,7 @@ namespace MediaBrowser.Controller.Entities.TV
};
if (user != null)
{
- var config = user.Configuration;
- if (!config.DisplayMissingEpisodes)
+ if (!user.DisplayMissingEpisodes)
{
query.IsMissing = false;
}
@@ -452,9 +447,9 @@ namespace MediaBrowser.Controller.Entities.TV
}
- protected override bool GetBlockUnratedValue(UserPolicy config)
+ protected override bool GetBlockUnratedValue(User user)
{
- return config.BlockUnratedItems.Contains(UnratedItem.Series);
+ return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString());
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 7159b5b1a..c327d17c9 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs
deleted file mode 100644
index 53601a610..000000000
--- a/MediaBrowser.Controller/Entities/User.cs
+++ /dev/null
@@ -1,262 +0,0 @@
-using System;
-using System.Globalization;
-using System.IO;
-using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Users;
-
-namespace MediaBrowser.Controller.Entities
-{
- /// <summary>
- /// Class User
- /// </summary>
- public class User : BaseItem
- {
- public static IUserManager UserManager { get; set; }
-
- /// <summary>
- /// Gets or sets the password.
- /// </summary>
- /// <value>The password.</value>
- public string Password { get; set; }
- public string EasyPassword { get; set; }
-
- // Strictly to remove JsonIgnore
- public override ItemImageInfo[] ImageInfos
- {
- get => base.ImageInfos;
- set => base.ImageInfos = value;
- }
-
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- [JsonIgnore]
- public override string Path
- {
- get => ConfigurationDirectoryPath;
- set => base.Path = value;
- }
-
- private string _name;
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public override string Name
- {
- get => _name;
- set
- {
- _name = value;
-
- // lazy load this again
- SortName = null;
- }
- }
-
- /// <summary>
- /// Returns the folder containing the item.
- /// If the item is a folder, it returns the folder itself
- /// </summary>
- /// <value>The containing folder path.</value>
- [JsonIgnore]
- public override string ContainingFolderPath => Path;
-
- /// <summary>
- /// Gets the root folder.
- /// </summary>
- /// <value>The root folder.</value>
- [JsonIgnore]
- public Folder RootFolder => LibraryManager.GetUserRootFolder();
-
- /// <summary>
- /// Gets or sets the last login date.
- /// </summary>
- /// <value>The last login date.</value>
- public DateTime? LastLoginDate { get; set; }
- /// <summary>
- /// Gets or sets the last activity date.
- /// </summary>
- /// <value>The last activity date.</value>
- public DateTime? LastActivityDate { get; set; }
-
- private volatile UserConfiguration _config;
- private readonly object _configSyncLock = new object();
- [JsonIgnore]
- public UserConfiguration Configuration
- {
- get
- {
- if (_config == null)
- {
- lock (_configSyncLock)
- {
- if (_config == null)
- {
- _config = UserManager.GetUserConfiguration(this);
- }
- }
- }
-
- return _config;
- }
- set => _config = value;
- }
-
- private volatile UserPolicy _policy;
- private readonly object _policySyncLock = new object();
- [JsonIgnore]
- public UserPolicy Policy
- {
- get
- {
- if (_policy == null)
- {
- lock (_policySyncLock)
- {
- if (_policy == null)
- {
- _policy = UserManager.GetUserPolicy(this);
- }
- }
- }
-
- return _policy;
- }
- set => _policy = value;
- }
-
- /// <summary>
- /// Renames the user.
- /// </summary>
- /// <param name="newName">The new name.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
- public Task Rename(string newName)
- {
- if (string.IsNullOrWhiteSpace(newName))
- {
- throw new ArgumentException("Username can't be empty", nameof(newName));
- }
-
- Name = newName;
-
- return RefreshMetadata(
- new MetadataRefreshOptions(new DirectoryService(FileSystem))
- {
- ReplaceAllMetadata = true,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ForceSave = true
-
- },
- CancellationToken.None);
- }
-
- public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
- {
- UserManager.UpdateUser(this);
- }
-
- /// <summary>
- /// Gets the path to the user's configuration directory
- /// </summary>
- /// <value>The configuration directory path.</value>
- [JsonIgnore]
- public string ConfigurationDirectoryPath => GetConfigurationDirectoryPath(Name);
-
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- return 1;
- }
-
- /// <summary>
- /// Gets the configuration directory path.
- /// </summary>
- /// <param name="username">The username.</param>
- /// <returns>System.String.</returns>
- private string GetConfigurationDirectoryPath(string username)
- {
- var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath;
-
- // TODO: Remove idPath and just use usernamePath for future releases
- var usernamePath = System.IO.Path.Combine(parentPath, username);
- var idPath = System.IO.Path.Combine(parentPath, Id.ToString("N", CultureInfo.InvariantCulture));
- if (!Directory.Exists(usernamePath) && Directory.Exists(idPath))
- {
- Directory.Move(idPath, usernamePath);
- }
-
- return usernamePath;
- }
-
- public bool IsParentalScheduleAllowed()
- {
- return IsParentalScheduleAllowed(DateTime.UtcNow);
- }
-
- public bool IsParentalScheduleAllowed(DateTime date)
- {
- var schedules = Policy.AccessSchedules;
-
- if (schedules.Length == 0)
- {
- return true;
- }
-
- foreach (var i in schedules)
- {
- if (IsParentalScheduleAllowed(i, date))
- {
- return true;
- }
- }
- return false;
- }
-
- private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
- {
- if (date.Kind != DateTimeKind.Utc)
- {
- throw new ArgumentException("Utc date expected");
- }
-
- var localTime = date.ToLocalTime();
-
- return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) &&
- IsWithinTime(schedule, localTime);
- }
-
- private bool IsWithinTime(AccessSchedule schedule, DateTime localTime)
- {
- var hour = localTime.TimeOfDay.TotalHours;
-
- return hour >= schedule.StartHour && hour <= schedule.EndHour;
- }
-
- public bool IsFolderGrouped(Guid id)
- {
- foreach (var i in Configuration.GroupedFolders)
- {
- if (new Guid(i) == id)
- {
- return true;
- }
- }
- return false;
- }
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
- public long InternalId { get; set; }
-
-
- }
-}
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 8a68f830c..39f4e0b6c 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index 86cff632c..ceca77bc0 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
@@ -111,7 +112,7 @@ namespace MediaBrowser.Controller.Entities
private static string[] UserSpecificViewTypes = new string[]
{
- MediaBrowser.Model.Entities.CollectionType.Playlists
+ Model.Entities.CollectionType.Playlists
};
public static bool IsUserSpecific(Folder folder)
@@ -140,8 +141,8 @@ namespace MediaBrowser.Controller.Entities
private static string[] ViewTypesEligibleForGrouping = new string[]
{
- MediaBrowser.Model.Entities.CollectionType.Movies,
- MediaBrowser.Model.Entities.CollectionType.TvShows,
+ Model.Entities.CollectionType.Movies,
+ Model.Entities.CollectionType.TvShows,
string.Empty
};
@@ -152,12 +153,12 @@ namespace MediaBrowser.Controller.Entities
private static string[] OriginalFolderViewTypes = new string[]
{
- MediaBrowser.Model.Entities.CollectionType.Books,
- MediaBrowser.Model.Entities.CollectionType.MusicVideos,
- MediaBrowser.Model.Entities.CollectionType.HomeVideos,
- MediaBrowser.Model.Entities.CollectionType.Photos,
- MediaBrowser.Model.Entities.CollectionType.Music,
- MediaBrowser.Model.Entities.CollectionType.BoxSets
+ Model.Entities.CollectionType.Books,
+ Model.Entities.CollectionType.MusicVideos,
+ Model.Entities.CollectionType.HomeVideos,
+ Model.Entities.CollectionType.Photos,
+ Model.Entities.CollectionType.Music,
+ Model.Entities.CollectionType.BoxSets
};
public static bool EnableOriginalFolder(string viewType)
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index db3e67f0d..bf0e56cf7 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -2,14 +2,19 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Controller.Entities
{
@@ -140,14 +145,15 @@ namespace MediaBrowser.Controller.Entities
return parent.QueryRecursive(query);
}
- var list = new List<BaseItem>();
-
- list.Add(GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent));
- list.Add(GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent));
- list.Add(GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent));
- list.Add(GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent));
- list.Add(GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent));
- list.Add(GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent));
+ var list = new List<BaseItem>
+ {
+ GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent),
+ GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent),
+ GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent),
+ GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent),
+ GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent),
+ GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)
+ };
return GetResult(list, parent, query);
}
@@ -293,21 +299,27 @@ namespace MediaBrowser.Controller.Entities
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Series).Name, typeof(Season).Name, typeof(Episode).Name };
+ query.IncludeItemTypes = new[]
+ {
+ nameof(Series),
+ nameof(Season),
+ nameof(Episode)
+ };
}
return parent.QueryRecursive(query);
}
- var list = new List<BaseItem>();
-
- list.Add(GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent));
- list.Add(GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent));
- list.Add(GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent));
- list.Add(GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent));
- list.Add(GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent));
- list.Add(GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent));
- list.Add(GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent));
+ var list = new List<BaseItem>
+ {
+ GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent),
+ GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent),
+ GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent),
+ GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent),
+ GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent),
+ GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent),
+ GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)
+ };
return GetResult(list, parent, query);
}
@@ -417,7 +429,8 @@ namespace MediaBrowser.Controller.Entities
};
}
- private QueryResult<BaseItem> GetResult<T>(IEnumerable<T> items,
+ private QueryResult<BaseItem> GetResult<T>(
+ IEnumerable<T> items,
BaseItem queryParent,
InternalItemsQuery query)
where T : BaseItem
diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs
index d9d1ca8c7..aa7001611 100644
--- a/MediaBrowser.Controller/Library/IIntroProvider.cs
+++ b/MediaBrowser.Controller/Library/IIntroProvider.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable{System.String}.</returns>
- Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user);
+ Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user);
/// <summary>
/// Gets all intro files.
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 5afe356f4..d7237039e 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -2,10 +2,10 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
@@ -14,6 +14,9 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
namespace MediaBrowser.Controller.Library
{
@@ -289,7 +292,8 @@ namespace MediaBrowser.Controller.Library
/// <param name="parentId">The parent identifier.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
- UserView GetNamedView(User user,
+ UserView GetNamedView(
+ User user,
string name,
Guid parentId,
string viewType,
@@ -302,7 +306,8 @@ namespace MediaBrowser.Controller.Library
/// <param name="name">The name.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
- UserView GetNamedView(User user,
+ UserView GetNamedView(
+ User user,
string name,
string viewType,
string sortName);
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 0ceabd0e6..94528ff77 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index 554dd0895..36b250ec9 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index eb735d31a..f5ccad671 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
@@ -27,7 +28,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="reason">The reason.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
- void SaveUserData(User userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
+ void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
UserItemData GetUserData(User user, BaseItem item);
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index be7b4ce59..b5b2e4729 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
-using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Events;
@@ -17,36 +16,46 @@ namespace MediaBrowser.Controller.Library
public interface IUserManager
{
/// <summary>
- /// Gets the users.
+ /// Occurs when a user is updated.
/// </summary>
- /// <value>The users.</value>
- IEnumerable<User> Users { get; }
+ event EventHandler<GenericEventArgs<User>> OnUserUpdated;
/// <summary>
- /// Gets the user ids.
+ /// Occurs when a user is created.
/// </summary>
- /// <value>The users ids.</value>
- IEnumerable<Guid> UsersIds { get; }
+ event EventHandler<GenericEventArgs<User>> OnUserCreated;
/// <summary>
- /// Occurs when [user updated].
+ /// Occurs when a user is deleted.
/// </summary>
- event EventHandler<GenericEventArgs<User>> UserUpdated;
+ event EventHandler<GenericEventArgs<User>> OnUserDeleted;
/// <summary>
- /// Occurs when [user deleted].
+ /// Occurs when a user's password is changed.
/// </summary>
- event EventHandler<GenericEventArgs<User>> UserDeleted;
+ event EventHandler<GenericEventArgs<User>> OnUserPasswordChanged;
- event EventHandler<GenericEventArgs<User>> UserCreated;
-
- event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
+ /// <summary>
+ /// Occurs when a user is locked out.
+ /// </summary>
+ event EventHandler<GenericEventArgs<User>> OnUserLockedOut;
- event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
+ /// <summary>
+ /// Gets the users.
+ /// </summary>
+ /// <value>The users.</value>
+ IEnumerable<User> Users { get; }
- event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
+ /// <summary>
+ /// Gets the user ids.
+ /// </summary>
+ /// <value>The users ids.</value>
+ IEnumerable<Guid> UsersIds { get; }
- event EventHandler<GenericEventArgs<User>> UserLockedOut;
+ /// <summary>
+ /// Initializes the user manager and ensures that a user exists.
+ /// </summary>
+ void Initialize();
/// <summary>
/// Gets a user by Id.
@@ -64,13 +73,6 @@ namespace MediaBrowser.Controller.Library
User GetUserByName(string name);
/// <summary>
- /// Refreshes metadata for each user
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task RefreshUsersMetadata(CancellationToken cancellationToken);
-
- /// <summary>
/// Renames the user.
/// </summary>
/// <param name="user">The user.</param>
@@ -89,19 +91,27 @@ namespace MediaBrowser.Controller.Library
void UpdateUser(User user);
/// <summary>
- /// Creates the user.
+ /// Updates the user.
/// </summary>
- /// <param name="name">The name.</param>
- /// <returns>User.</returns>
+ /// <param name="user">The user.</param>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
+ /// <returns>A task representing the update of the user.</returns>
+ Task UpdateUserAsync(User user);
+
+ /// <summary>
+ /// Creates a user with the specified name.
+ /// </summary>
+ /// <param name="name">The name of the new user.</param>
+ /// <returns>The created user.</returns>
/// <exception cref="ArgumentNullException">name</exception>
/// <exception cref="ArgumentException"></exception>
User CreateUser(string name);
/// <summary>
- /// Deletes the user.
+ /// Deletes the specified user.
/// </summary>
- /// <param name="user">The user.</param>
- /// <returns>Task.</returns>
+ /// <param name="user">The user to be deleted.</param>
void DeleteUser(User user);
/// <summary>
@@ -112,13 +122,6 @@ namespace MediaBrowser.Controller.Library
Task ResetPassword(User user);
/// <summary>
- /// Gets the offline user dto.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns>UserDto.</returns>
- UserDto GetOfflineUserDto(User user);
-
- /// <summary>
/// Resets the easy password.
/// </summary>
/// <param name="user">The user.</param>
@@ -163,47 +166,34 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
- /// <summary>
- /// Gets the user policy.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns>UserPolicy.</returns>
- UserPolicy GetUserPolicy(User user);
+ void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
- /// <summary>
- /// Gets the user configuration.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns>UserConfiguration.</returns>
- UserConfiguration GetUserConfiguration(User user);
+ NameIdPair[] GetAuthenticationProviders();
+
+ NameIdPair[] GetPasswordResetProviders();
/// <summary>
- /// Updates the configuration.
+ /// This method updates the user's configuration.
+ /// This is only included as a stopgap until the new API, using this internally is not recommended.
+ /// Instead, modify the user object directly, then call <see cref="UpdateUser"/>.
/// </summary>
- /// <param name="userId">The user identifier.</param>
- /// <param name="newConfiguration">The new configuration.</param>
- /// <returns>Task.</returns>
- void UpdateConfiguration(Guid userId, UserConfiguration newConfiguration);
-
- void UpdateConfiguration(User user, UserConfiguration newConfiguration);
+ /// <param name="userId">The user's Id.</param>
+ /// <param name="config">The request containing the new user configuration.</param>
+ void UpdateConfiguration(Guid userId, UserConfiguration config);
/// <summary>
- /// Updates the user policy.
+ /// This method updates the user's policy.
+ /// This is only included as a stopgap until the new API, using this internally is not recommended.
+ /// Instead, modify the user object directly, then call <see cref="UpdateUser"/>.
/// </summary>
- /// <param name="userId">The user identifier.</param>
- /// <param name="userPolicy">The user policy.</param>
- void UpdateUserPolicy(Guid userId, UserPolicy userPolicy);
+ /// <param name="userId">The user's Id.</param>
+ /// <param name="policy">The request containing the new user policy.</param>
+ void UpdatePolicy(Guid userId, UserPolicy policy);
/// <summary>
- /// Makes the valid username.
+ /// Clears the user's profile image.
/// </summary>
- /// <param name="username">The username.</param>
- /// <returns>System.String.</returns>
- string MakeValidUsername(string username);
-
- void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
-
- NameIdPair[] GetAuthenticationProviders();
- NameIdPair[] GetPasswordResetProviders();
+ /// <param name="user">The user.</param>
+ void ClearProfileImage(User user);
}
}
diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
index b0302d04c..b4e205184 100644
--- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
+++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index e02c387e4..bc3bf78f0 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index 60391bb83..10af98121 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index e89bc05aa..5b901d223 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 2d2f73148..8ce106469 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Configuration;
@@ -2148,7 +2149,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var user = state.User;
// If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
- if (user != null && !user.Policy.EnableVideoPlaybackTranscoding)
+ if (user != null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
state.OutputVideoCodec = "copy";
}
@@ -2164,7 +2165,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var user = state.User;
// If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
- if (user != null && !user.Policy.EnableAudioPlaybackTranscoding)
+ if (user != null && !user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
state.OutputAudioCodec = "copy";
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 7cd783595..acf1aae89 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -18,22 +18,37 @@ namespace MediaBrowser.Controller.MediaEncoding
public class EncodingJobInfo
{
public MediaStream VideoStream { get; set; }
+
public VideoType VideoType { get; set; }
+
public Dictionary<string, string> RemoteHttpHeaders { get; set; }
+
public string OutputVideoCodec { get; set; }
+
public MediaProtocol InputProtocol { get; set; }
+
public string MediaPath { get; set; }
+
public bool IsInputVideo { get; set; }
+
public IIsoMount IsoMount { get; set; }
+
public string[] PlayableStreamFileNames { get; set; }
+
public string OutputAudioCodec { get; set; }
+
public int? OutputVideoBitrate { get; set; }
+
public MediaStream SubtitleStream { get; set; }
+
public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+
public string[] SupportedSubtitleCodecs { get; set; }
public int InternalSubtitleStreamOffset { get; set; }
+
public MediaSourceInfo MediaSource { get; set; }
+
public User User { get; set; }
public long? RunTimeTicks { get; set; }
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
index 3e004763d..4361e253b 100644
--- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -1,36 +1,40 @@
using System;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Net
{
public class AuthorizationInfo
{
/// <summary>
- /// Gets or sets the user identifier.
+ /// Gets the user identifier.
/// </summary>
/// <value>The user identifier.</value>
- public Guid UserId => User == null ? Guid.Empty : User.Id;
+ public Guid UserId => User?.Id ?? Guid.Empty;
/// <summary>
/// Gets or sets the device identifier.
/// </summary>
/// <value>The device identifier.</value>
public string DeviceId { get; set; }
+
/// <summary>
/// Gets or sets the device.
/// </summary>
/// <value>The device.</value>
public string Device { get; set; }
+
/// <summary>
/// Gets or sets the client.
/// </summary>
/// <value>The client.</value>
public string Client { get; set; }
+
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
public string Version { get; set; }
+
/// <summary>
/// Gets or sets the token.
/// </summary>
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 7dca793c6..f09cb3705 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -156,6 +156,7 @@ namespace MediaBrowser.Controller.Net
await connection.SendAsync(
new WebSocketMessage<TReturnDataType>
{
+ MessageId = Guid.NewGuid(),
MessageType = Name,
Data = data
},
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index 9132404a0..2055a656a 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -1,14 +1,36 @@
#nullable enable
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// IAuthService.
+ /// </summary>
public interface IAuthService
{
- void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
- User? Authenticate(HttpRequest 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);
+
+ /// <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.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>Authorization information. Null if unauthenticated.</returns>
+ AuthorizationInfo Authenticate(HttpRequest request);
}
}
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
index 61598391f..37a7425b9 100644
--- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -1,7 +1,11 @@
using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// IAuthorization context.
+ /// </summary>
public interface IAuthorizationContext
{
/// <summary>
@@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
+
+ /// <summary>
+ /// Gets the authorization information.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <returns>AuthorizationInfo.</returns>
+ AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
}
}
diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs
index 5c3c19f6b..421ac3fe2 100644
--- a/MediaBrowser.Controller/Net/ISessionContext.cs
+++ b/MediaBrowser.Controller/Net/ISessionContext.cs
@@ -1,4 +1,4 @@
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services;
diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs
index 8c6019923..ab5eb13cd 100644
--- a/MediaBrowser.Controller/Notifications/INotificationService.cs
+++ b/MediaBrowser.Controller/Notifications/INotificationService.cs
@@ -1,6 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Notifications
{
diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs
index 3f46468b3..a1029589b 100644
--- a/MediaBrowser.Controller/Notifications/UserNotification.cs
+++ b/MediaBrowser.Controller/Notifications/UserNotification.cs
@@ -1,5 +1,5 @@
using System;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Notifications;
namespace MediaBrowser.Controller.Notifications
diff --git a/MediaBrowser.Controller/Persistence/IUserRepository.cs b/MediaBrowser.Controller/Persistence/IUserRepository.cs
deleted file mode 100644
index cd23e5223..000000000
--- a/MediaBrowser.Controller/Persistence/IUserRepository.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Collections.Generic;
-using MediaBrowser.Controller.Entities;
-
-namespace MediaBrowser.Controller.Persistence
-{
- /// <summary>
- /// Provides an interface to implement a User repository
- /// </summary>
- public interface IUserRepository : IRepository
- {
- /// <summary>
- /// Deletes the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns>Task.</returns>
- void DeleteUser(User user);
-
- /// <summary>
- /// Retrieves all users.
- /// </summary>
- /// <returns>IEnumerable{User}.</returns>
- List<User> RetrieveAllUsers();
-
- void CreateUser(User user);
- void UpdateUser(User user);
- }
-}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 3b08e72b9..b1a638883 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -241,15 +242,7 @@ namespace MediaBrowser.Controller.Playlists
}
var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
- foreach (var share in shares)
- {
- if (string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
+ return shares.Any(share => string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase));
}
public override bool IsVisibleStandalone(User user)
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 254b27460..955db0278 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -70,6 +71,8 @@ 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);
+
/// <summary>
/// Adds the metadata providers.
/// </summary>
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 4c2f834cb..1fdb588eb 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Dto;
@@ -75,7 +74,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="deviceName">Name of the device.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
- SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user);
+ SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
void UpdateDeviceName(string sessionId, string reportedDeviceName);
diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
index 1e2df37bf..6f75d16de 100644
--- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
@@ -1,4 +1,3 @@
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Sorting
@@ -12,7 +11,7 @@ namespace MediaBrowser.Controller.Sorting
/// Gets or sets the user.
/// </summary>
/// <value>The user.</value>
- User User { get; set; }
+ Jellyfin.Data.Entities.User User { get; set; }
/// <summary>
/// Gets or sets the user manager.
diff --git a/MediaBrowser.Model/Configuration/AccessSchedule.cs b/MediaBrowser.Model/Configuration/AccessSchedule.cs
index 120c47dbc..7bd355449 100644
--- a/MediaBrowser.Model/Configuration/AccessSchedule.cs
+++ b/MediaBrowser.Model/Configuration/AccessSchedule.cs
@@ -1,3 +1,5 @@
+using Jellyfin.Data.Enums;
+
#pragma warning disable CS1591
namespace MediaBrowser.Model.Configuration
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
index 289047d6b..85d864eec 100644
--- a/MediaBrowser.Model/Configuration/UserConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Configuration
{
diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs
index d89a4b3ad..6ea9ee419 100644
--- a/MediaBrowser.Model/Entities/ImageType.cs
+++ b/MediaBrowser.Model/Entities/ImageType.cs
@@ -63,6 +63,11 @@ namespace MediaBrowser.Model.Entities
/// <summary>
/// The box rear.
/// </summary>
- BoxRear = 11
+ BoxRear = 11,
+
+ /// <summary>
+ /// The user profile image.
+ /// </summary>
+ Profile = 12
}
}
diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs
index 9e570ef1c..239a3777e 100644
--- a/MediaBrowser.Model/Notifications/NotificationOptions.cs
+++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs
@@ -2,7 +2,10 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Extensions;
using System.Linq;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Model.Notifications
@@ -104,7 +107,7 @@ namespace MediaBrowser.Model.Notifications
!opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase);
}
- public bool IsEnabledToSendToUser(string type, string userId, UserPolicy userPolicy)
+ public bool IsEnabledToSendToUser(string type, string userId, User user)
{
NotificationOption opt = GetOptions(type);
@@ -115,7 +118,7 @@ namespace MediaBrowser.Model.Notifications
return true;
}
- if (opt.SendToUserMode == SendToUserType.Admins && userPolicy.IsAdministrator)
+ if (opt.SendToUserMode == SendToUserType.Admins && user.HasPermission(PermissionKind.IsAdministrator))
{
return true;
}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index e6c5cbe66..2fd27d3b9 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -2,7 +2,9 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Model.Configuration;
+using System.Xml.Serialization;
+using Jellyfin.Data.Enums;
+using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
namespace MediaBrowser.Model.Users
{
@@ -78,6 +80,8 @@ namespace MediaBrowser.Model.Users
public string[] BlockedChannels { get; set; }
public int RemoteClientBitrateLimit { get; set; }
+
+ [XmlElement(ElementName = "AuthenticationProviderId")]
public string AuthenticationProviderId { get; set; }
public string PasswordResetProviderId { get; set; }
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 3ab621ba4..3c94f6215 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -5,17 +5,21 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Person = MediaBrowser.Controller.Entities.Person;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
namespace MediaBrowser.Providers.Manager
{
@@ -78,11 +82,6 @@ namespace MediaBrowser.Providers.Manager
var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && !(item is Audio);
- if (item is User)
- {
- saveLocally = true;
- }
-
if (type != ImageType.Primary && item is Episode)
{
saveLocally = false;
@@ -132,11 +131,11 @@ namespace MediaBrowser.Providers.Manager
var currentImage = GetCurrentImage(item, type, index);
var currentImageIsLocalFile = currentImage != null && currentImage.IsLocalFile;
- var currentImagePath = currentImage == null ? null : currentImage.Path;
+ var currentImagePath = currentImage?.Path;
var savedPaths = new List<string>();
- using (source)
+ await using (source)
{
var currentPathIndex = 0;
@@ -172,7 +171,6 @@ namespace MediaBrowser.Providers.Manager
}
catch (FileNotFoundException)
{
-
}
finally
{
@@ -181,6 +179,11 @@ namespace MediaBrowser.Providers.Manager
}
}
+ public async Task SaveImage(User user, Stream source, string path)
+ {
+ await SaveImageToLocation(source, path, path, CancellationToken.None).ConfigureAwait(false);
+ }
+
private async Task<string> SaveImageToLocation(Stream source, string path, string retryPath, CancellationToken cancellationToken)
{
try
@@ -244,7 +247,7 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
- using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+ 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);
}
@@ -439,7 +442,6 @@ namespace MediaBrowser.Providers.Manager
{
path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension);
}
-
else if (item.IsInMixedFolder)
{
path = GetSavePathForItemInMixedFolder(item, type, filename, extension);
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 13a4436e1..eeb74ec96 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -16,7 +17,6 @@ using MediaBrowser.Controller.Dto;
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.Controller.Subtitles;
@@ -28,6 +28,12 @@ using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using Priority_Queue;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Providers.Manager
{
@@ -182,6 +188,12 @@ 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)
+ {
+ return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
+ .SaveImage(user, source, path);
+ }
+
public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
{
var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
diff --git a/MediaBrowser.Providers/Users/UserMetadataService.cs b/MediaBrowser.Providers/Users/UserMetadataService.cs
deleted file mode 100644
index ad4d315f5..000000000
--- a/MediaBrowser.Providers/Users/UserMetadataService.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Providers.Manager;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Users
-{
- public class UserMetadataService : MetadataService<User, ItemLookupInfo>
- {
- public UserMetadataService(
- IServerConfigurationManager serverConfigurationManager,
- ILogger<UserMetadataService> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
- : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
- {
- }
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<User> source, MetadataResult<User> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
- }
-}
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.sln b/MediaBrowser.sln
index e100c0b1c..0362eff1c 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "Medi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}"
@@ -94,10 +92,6 @@ Global
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU
{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index 437dfa410..4ea5094b6 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -1,13 +1,13 @@
using System;
using System.Linq;
using System.Security.Claims;
-using System.Text.Encodings.Web;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
@@ -24,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
private readonly IFixture _fixture;
private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
- private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
- private readonly Mock<ISystemClock> _clockMock;
- private readonly Mock<IServiceProvider> _serviceProviderMock;
- private readonly Mock<IAuthenticationService> _authenticationServiceMock;
- private readonly UrlEncoder _urlEncoder;
- private readonly HttpContext _context;
private readonly CustomAuthenticationHandler _sut;
private readonly AuthenticationScheme _scheme;
@@ -45,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
AllowFixtureCircularDependencies();
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
- _optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
- _clockMock = _fixture.Freeze<Mock<ISystemClock>>();
- _serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
- _authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
+ var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
+ var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
+ var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
- _urlEncoder = UrlEncoder.Default;
+ serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
+ .Returns(authenticationServiceMock.Object);
- _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
- .Returns(_authenticationServiceMock.Object);
-
- _optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
+ optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
.Returns(new AuthenticationSchemeOptions
{
ForwardAuthenticate = null
});
- _context = new DefaultHttpContext
+ HttpContext context = new DefaultHttpContext
{
- RequestServices = _serviceProviderMock.Object
+ RequestServices = serviceProviderMock.Object
};
_scheme = new AuthenticationScheme(
@@ -73,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
typeof(CustomAuthenticationHandler));
_sut = _fixture.Create<CustomAuthenticationHandler>();
- _sut.InitializeAsync(_scheme, _context).Wait();
- }
-
- [Fact]
- public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
- {
- _jellyfinAuthServiceMock.Setup(
- a => a.Authenticate(
- It.IsAny<HttpRequest>(),
- It.IsAny<AuthenticatedAttribute>()))
- .Returns((User?)null);
-
- var authenticateResult = await _sut.AuthenticateAsync();
-
- Assert.False(authenticateResult.Succeeded);
- Assert.True(authenticateResult.None);
- // TODO return when legacy API is removed.
- // Assert.Equal("Invalid user", authenticateResult.Failure.Message);
+ _sut.InitializeAsync(_scheme, context).Wait();
}
[Fact]
@@ -100,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
- It.IsAny<HttpRequest>(),
- It.IsAny<AuthenticatedAttribute>()))
+ It.IsAny<HttpRequest>()))
.Throws(new SecurityException(errorMessage));
var authenticateResult = await _sut.AuthenticateAsync();
@@ -123,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
[Fact]
public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
{
- var user = SetupUser();
+ var authorizationInfo = SetupUser();
var authenticateResult = await _sut.AuthenticateAsync();
- Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Name));
+ Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
}
[Theory]
@@ -134,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
[InlineData(false)]
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
{
- var user = SetupUser(isAdmin);
+ var authorizationInfo = SetupUser(isAdmin);
var authenticateResult = await _sut.AuthenticateAsync();
- var expectedRole = user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User;
+ var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
}
@@ -150,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
}
- private User SetupUser(bool isAdmin = false)
+ private AuthorizationInfo SetupUser(bool isAdmin = false)
{
- var user = _fixture.Create<User>();
- user.Policy.IsAdministrator = isAdmin;
+ var authorizationInfo = _fixture.Create<AuthorizationInfo>();
+ authorizationInfo.User = _fixture.Create<User>();
+ authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
- It.IsAny<HttpRequest>(),
- It.IsAny<AuthenticatedAttribute>()))
- .Returns(user);
+ It.IsAny<HttpRequest>()))
+ .Returns(authorizationInfo);
- return user;
+ return authorizationInfo;
}
private void AllowFixtureCircularDependencies()
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..b65d45aa0
--- /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.IgnoreSchedulePolicy;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
+{
+ public class IgnoreScheduleHandlerTests
+ {
+ private readonly Mock<IConfigurationManager> _configurationManagerMock;
+ private readonly List<IAuthorizationRequirement> _requirements;
+ private readonly IgnoreScheduleHandler _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 IgnoreScheduleRequirement() };
+ _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+ _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+ _sut = fixture.Create<IgnoreScheduleHandler>();
+ }
+
+ [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
index b01d1af1f..397eb2edc 100644
--- a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
+++ b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
@@ -31,8 +31,8 @@ namespace Jellyfin.Api.Tests
var confManagerMock = Mock.Of<IServerConfigurationManager>(x => x.Configuration == conf);
- var service = new BrandingService(
- new NullLogger<BrandingService>(),
+ var service = new TestService(
+ new NullLogger<TestService>(),
confManagerMock,
Mock.Of<IHttpResultFactory>())
{
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 269ff0986..010fad520 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -24,9 +24,22 @@
<PackageReference Include="Moq" Version="4.14.1" />
</ItemGroup>
+ <!-- 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>
+
<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' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
</Project>
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 003e29a66..73af10065 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -19,8 +19,20 @@
<PackageReference Include="coverlet.collector" Version="1.2.1" />
</ItemGroup>
+ <!-- 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>
+
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
</ItemGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
</Project>
diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs
index 03523dbc4..46926f4f8 100644
--- a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs
+++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs
@@ -7,7 +7,8 @@ namespace Jellyfin.Common.Tests
public class PasswordHashTests
{
[Theory]
- [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
+ [InlineData(
+ "$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
"PBKDF2",
"",
"62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 3c806be17..5c7934fe3 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -19,8 +19,20 @@
<PackageReference Include="coverlet.collector" Version="1.2.1" />
</ItemGroup>
+ <!-- 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>
+
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
</ItemGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
</Project>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index e0f1f236c..af29fec87 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -9,20 +9,6 @@ namespace Jellyfin.MediaEncoding.Tests
{
public class EncoderValidatorTests
{
- private class GetFFmpegVersionTestData : IEnumerable<object?[]>
- {
- public IEnumerator<object?[]> GetEnumerator()
- {
- 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) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput, null };
- }
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
- }
-
[Theory]
[ClassData(typeof(GetFFmpegVersionTestData))]
public void GetFFmpegVersionTest(string versionOutput, Version? version)
@@ -41,5 +27,19 @@ namespace Jellyfin.MediaEncoding.Tests
var val = new EncoderValidator(new NullLogger<EncoderValidatorTests>());
Assert.Equal(valid, val.ValidateVersionInternal(versionOutput));
}
+
+ private class GetFFmpegVersionTestData : IEnumerable<object?[]>
+ {
+ public IEnumerator<object?[]> GetEnumerator()
+ {
+ 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) };
+ yield return new object?[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) };
+ yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput, null };
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index c2e7bf54c..74fc37e49 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -25,8 +25,20 @@
<PackageReference Include="coverlet.collector" Version="1.2.1" />
</ItemGroup>
+ <!-- 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>
+
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
</Project>
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index f6c327498..e93385136 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -14,8 +14,20 @@
<PackageReference Include="coverlet.collector" Version="1.2.1" />
</ItemGroup>
+ <!-- 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>
+
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
</Project>
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 43b34dcee..355949640 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -9,6 +9,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
@@ -21,11 +22,13 @@
<ItemGroup>
<ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" />
</ItemGroup>
-
+
<!-- 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' ">
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 3bad6bb7b..83aca38b4 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -5,25 +5,37 @@
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
- <IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
- </PropertyGroup>
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.11.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
+ <PackageReference Include="Moq" Version="4.14.1" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
+ <PackageReference Include="coverlet.collector" Version="1.2.1" />
+ </ItemGroup>
- <ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.11.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
- <PackageReference Include="Moq" Version="4.14.1" />
- <PackageReference Include="xunit" Version="2.4.1" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="1.2.1" />
- </ItemGroup>
+ <!-- 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>
- <ItemGroup>
- <ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
- </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
</Project>
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 c845ca6cf..597053131 100644
--- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
+++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
@@ -20,10 +20,12 @@
<ProjectReference Include="..\..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- 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' ">