aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml3
-rw-r--r--.ci/azure-pipelines-api-client.yml78
-rw-r--r--.ci/azure-pipelines-package.yml36
-rw-r--r--.ci/azure-pipelines-test.yml2
-rw-r--r--.ci/azure-pipelines.yml9
-rw-r--r--.gitignore1
-rw-r--r--.npmrc3
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs36
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs2
-rw-r--r--Emby.Dlna/DlnaManager.cs6
-rw-r--r--Emby.Dlna/Eventing/DlnaEventManager.cs2
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs4
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs18
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs13
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs122
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs48
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs12
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs18
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs16
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs45
-rw-r--r--Emby.Drawing/ImageProcessor.cs6
-rw-r--r--Emby.Drawing/NullImageEncoder.cs2
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs17
-rw-r--r--Emby.Notifications/NotificationEntryPoint.cs5
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs135
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs19
-rw-r--r--Emby.Server.Implementations/Channels/ChannelPostScanTask.cs2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs3
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs132
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs9
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj8
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs7
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs11
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs7
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs145
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs5
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs17
-rw-r--r--Emby.Server.Implementations/Images/ArtistImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs13
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs9
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs15
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs51
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs67
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs14
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs28
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs2
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs2
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs26
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs8
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_DO.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json41
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json121
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/nn.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json43
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json5
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs8
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs78
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs2
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs18
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs38
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs3
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs3
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs8
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs4
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs76
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs7
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs15
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs8
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs8
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs6
-rw-r--r--Jellyfin.Api/Constants/InternalClaimTypes.cs5
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs20
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs30
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs13
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs12
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs6
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs26
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs143
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs44
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs19
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs22
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs3
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs22
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs4
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs4
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs140
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs191
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ScheduledTasksController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs5
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs136
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs125
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs10
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs32
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs45
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs2
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs3
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs8
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs13
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs5
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs6
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs166
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs81
-rw-r--r--Jellyfin.Api/Helpers/SimilarItemsHelper.cs4
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs5
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj4
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs59
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs8
-rw-r--r--Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs34
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs14
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs14
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs9
-rw-r--r--Jellyfin.Data/Entities/User.cs5
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Data/Queries/ActivityLogQuery.cs30
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs4
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs64
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs46
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj5
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs464
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs28
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs5
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs1
-rw-r--r--Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs127
-rw-r--r--Jellyfin.Server/CoreAppHost.cs11
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs18
-rw-r--r--Jellyfin.Server/Filters/WebsocketModelFilter.cs30
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj12
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs4
-rw-r--r--Jellyfin.Server/Program.cs32
-rw-r--r--Jellyfin.Server/Startup.cs9
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs53
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs28
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs39
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs27
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs7
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj4
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs25
-rw-r--r--MediaBrowser.Common/Plugins/LocalPlugin.cs113
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs23
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs4
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs13
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs8
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs5
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs10
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs36
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs6
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs19
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs5
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs9
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs290
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs11
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs3
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs18
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs27
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketManager.cs6
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs4
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs8
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs4
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs9
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupInfo.cs52
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs10
-rw-r--r--MediaBrowser.Model/Activity/IActivityManager.cs14
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs5
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs2
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs7
-rw-r--r--MediaBrowser.Model/Dlna/AudioOptions.cs4
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs12
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs7
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs21
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs46
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs2
-rw-r--r--MediaBrowser.Model/Net/WebSocketMessage.cs3
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs4
-rw-r--r--MediaBrowser.Model/Session/GeneralCommandType.cs7
-rw-r--r--MediaBrowser.Model/Session/SessionMessageType.cs50
-rw-r--r--MediaBrowser.Model/Subtitles/FontFile.cs34
-rw-r--r--MediaBrowser.Model/System/PublicSystemInfo.cs7
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs4
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs8
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj9
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs75
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs29
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs30
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs143
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs254
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs80
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs78
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs71
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs309
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs212
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs22
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs128
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs457
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs302
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs34
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs102
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs247
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs113
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs222
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs156
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs107
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs241
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs160
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs527
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs469
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs118
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs43
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs69
-rw-r--r--MediaBrowser.Providers/TV/DummySeasonProvider.cs229
-rw-r--r--MediaBrowser.Providers/TV/MissingEpisodeProvider.cs404
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs41
-rw-r--r--apiclient/.openapi-generator-ignore2
-rw-r--r--apiclient/templates/typescript/axios/generate.sh19
-rw-r--r--apiclient/templates/typescript/axios/package.mustache30
-rw-r--r--debian/control1
-rw-r--r--debian/postrm4
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rwxr-xr-xdeployment/build.windows.amd644
-rw-r--r--fedora/jellyfin.spec10
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs6
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj12
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs225
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs17
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs2
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs92
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs92
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs32
-rw-r--r--tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs20
-rw-r--r--tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs19
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs21
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj6
-rw-r--r--windows/build-jellyfin.ps1190
-rw-r--r--windows/dependencies.txt2
-rw-r--r--windows/dialogs/confirmation.nsddef24
-rw-r--r--windows/dialogs/confirmation.nsdinc61
-rw-r--r--windows/dialogs/service-config.nsddef13
-rw-r--r--windows/dialogs/service-config.nsdinc56
-rw-r--r--windows/dialogs/setuptype.nsddef12
-rw-r--r--windows/dialogs/setuptype.nsdinc50
-rw-r--r--windows/helpers/ShowError.nsh10
-rw-r--r--windows/helpers/StrSlash.nsh47
-rw-r--r--windows/jellyfin.nsi575
-rw-r--r--windows/legacy/install-jellyfin.ps1460
-rw-r--r--windows/legacy/install.bat1
400 files changed, 5707 insertions, 8239 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 4d38a906e..b558d2a6f 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -62,6 +62,7 @@ jobs:
- task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact'
+ enabled: false
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
@@ -73,6 +74,7 @@ jobs:
- task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact'
+ enabled: false
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll'
@@ -83,6 +85,7 @@ jobs:
- task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool'
+ enabled: false
inputs:
command: custom
custom: compat
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
new file mode 100644
index 000000000..de6bbf04c
--- /dev/null
+++ b/.ci/azure-pipelines-api-client.yml
@@ -0,0 +1,78 @@
+parameters:
+ - name: LinuxImage
+ type: string
+ default: "ubuntu-latest"
+ - name: GeneratorVersion
+ type: string
+ default: "5.0.0-beta2"
+
+jobs:
+- job: GenerateApiClients
+ displayName: 'Generate Api Clients'
+ dependsOn: Test
+
+ pool:
+ vmImage: "${{ parameters.LinuxImage }}"
+
+ steps:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download OpenAPI Spec Artifact'
+ inputs:
+ source: 'current'
+ artifact: "OpenAPI Spec"
+ path: "$(System.ArtifactsDirectory)/openapispec"
+ runVersion: "latest"
+
+ - task: CmdLine@2
+ displayName: 'Download OpenApi Generator'
+ inputs:
+ script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
+
+## Authenticate with npm registry
+ - task: npmAuthenticate@0
+ inputs:
+ workingFile: ./.npmrc
+ customEndpoint: 'jellyfin-bot for NPM'
+
+## Generate npm api client
+# Unstable
+ - task: CmdLine@2
+ displayName: 'Build unstable typescript axios client'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+ inputs:
+ script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
+
+# Stable
+ - task: CmdLine@2
+ displayName: 'Build stable typescript axios client'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+ inputs:
+ script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
+
+## Run npm install
+ - task: Npm@1
+ displayName: 'Install npm dependencies'
+ inputs:
+ command: install
+ workingDir: ./apiclient/generated/typescript/axios
+
+## Publish npm packages
+# Unstable
+ - task: Npm@1
+ displayName: 'Publish unstable typescript axios client'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+ inputs:
+ command: publish
+ publishRegistry: useFeed
+ publishFeed: 'jellyfin/unstable'
+ workingDir: ./apiclient/generated/typescript/axios
+
+# Stable
+ - task: Npm@1
+ displayName: 'Publish stable typescript axios client'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+ inputs:
+ command: publish
+ publishRegistry: useExternalRegistry
+ publishEndpoint: 'jellyfin-bot for NPM'
+ workingDir: ./apiclient/generated/typescript/axios
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index cc845afd4..0dc604a79 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -65,6 +65,38 @@ jobs:
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+- job: OpenAPISpec
+ dependsOn: Test
+ condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
+ displayName: 'Push OpenAPI Spec to repository'
+
+ pool:
+ vmImage: 'ubuntu-latest'
+
+ steps:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download OpenAPI Spec'
+ inputs:
+ source: 'current'
+ artifact: "OpenAPI Spec"
+ path: "$(System.ArtifactsDirectory)/openapispec"
+ runVersion: "latest"
+
+ - task: SSH@0
+ displayName: 'Create target directory on repository server'
+ inputs:
+ sshEndpoint: repository
+ runOptions: 'inline'
+ inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
+
+ - task: CopyFilesOverSSH@0
+ displayName: 'Upload artifacts to repository server'
+ inputs:
+ sshEndpoint: repository
+ sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
+ contents: 'openapi.json'
+ targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
+
- job: BuildDocker
displayName: 'Build Docker'
@@ -135,7 +167,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
displayName: 'Update Stable Repository'
@@ -144,7 +176,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index eca8aa90f..6a36698b5 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -56,7 +56,7 @@ jobs:
inputs:
command: "test"
projects: ${{ parameters.TestProjects }}
- arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
+ arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
publishTestResults: true
testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)"
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index b417aae67..5b5a17dea 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -34,6 +34,12 @@ jobs:
Linux: 'ubuntu-latest'
Windows: 'windows-latest'
macOS: 'macos-latest'
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+ - template: azure-pipelines-test.yml
+ parameters:
+ ImageNames:
+ Linux: 'ubuntu-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml
@@ -55,3 +61,6 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+ - template: azure-pipelines-api-client.yml
diff --git a/.gitignore b/.gitignore
index 0df7606ce..7cd3d0068 100644
--- a/.gitignore
+++ b/.gitignore
@@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
+apiclient/generated
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..b7a317000
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+registry=https://registry.npmjs.org/
+@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
+always-auth=true \ No newline at end of file
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 99060d0b0..7b4772730 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -137,6 +137,7 @@
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
+ - [skyfrk](https://github.com/skyfrk)
# Emby Contributors
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 4b108b89e..5f25b8cdc 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
User = user,
Recursive = true,
IsMissing = false,
- ExcludeItemTypes = new[] { typeof(Book).Name },
+ ExcludeItemTypes = new[] { nameof(Book) },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
@@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
- ExcludeItemTypes = new[] { typeof(Book).Name },
+ ExcludeItemTypes = new[] { nameof(Book) },
IsPlaceHolder = false,
DtoOptions = GetDtoOptions()
};
@@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
StartIndex = startIndex,
Limit = limit,
};
- query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name };
+ query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
SetSorting(query, sort, false);
@@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
@@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
@@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
// query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+ query.IncludeItemTypes = new[] { nameof(BoxSet) };
var result = _libraryManager.GetItemsResult(query);
@@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
@@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Audio).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
@@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Audio).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
@@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
@@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
var result = _libraryManager.GetItemsResult(query);
@@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
@@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
@@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
{
UserId = user.Id,
Limit = 50,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
},
@@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
{
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
{
- stubType = (StubType)Enum.Parse(typeof(StubType), name, true);
- id = id.Split(new[] { '_' }, 2)[1];
+ stubType = Enum.Parse<StubType>(name, true);
+ id = id.Split('_', 2)[1];
break;
}
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 5b8a89d8f..abaf522bc 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
{
foreach (var att in profile.XmlRootAttributes)
{
- var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+ var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 1807ac6a1..069400833 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -383,9 +383,9 @@ namespace Emby.Dlna
continue;
}
- var filename = Path.GetFileName(name).Substring(namespaceName.Length);
-
- var path = Path.Combine(systemProfilesPath, filename);
+ var path = Path.Join(
+ systemProfilesPath,
+ Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
using (var stream = _assembly.GetManifestResourceStream(name))
{
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index 7d8da86ef..770d56c30 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
builder.Append("</e:propertyset>");
- using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
+ using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 40c2cc0e0..f8a00efac 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints()
{
- var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
+ var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
var udn = CreateUuid(_appHost.SystemId);
+ var descriptorUri = "/dlna/" + udn + "/description.xml";
foreach (var address in addresses)
{
@@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
- var descriptorUri = "/dlna/" + udn + "/description.xml";
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
var device = new SsdpRootDevice
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
index 8bf0cd961..464f71a6f 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Xml;
@@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
+ /// <summary>
+ /// Defines the <see cref="ControlHandler" />.
+ /// </summary>
public class ControlHandler : BaseControlHandler
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ControlHandler"/> class.
+ /// </summary>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(IServerConfigurationManager config, ILogger logger)
: base(config, logger)
{
@@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
}
+ /// <summary>
+ /// Records that the handle is authorized in the xml stream.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsAuthorized(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1");
+ /// <summary>
+ /// Records that the handle is validated in the xml stream.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsValidated(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1");
}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
index e6d845e1e..a5aae515c 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System.Net.Http;
using System.Threading.Tasks;
using Emby.Dlna.Service;
@@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
+ /// <summary>
+ /// Defines the <see cref="MediaReceiverRegistrarService" />.
+ /// </summary>
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
{
private readonly IServerConfigurationManager _config;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
+ /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
public MediaReceiverRegistrarService(
ILogger<MediaReceiverRegistrarService> logger,
IHttpClientFactory httpClientFactory,
@@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
/// <inheritdoc />
public string GetServiceXml()
{
- return new MediaReceiverRegistrarXmlBuilder().GetXml();
+ return MediaReceiverRegistrarXmlBuilder.GetXml();
}
/// <inheritdoc />
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
index 26994925d..37840cd09 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
@@ -1,79 +1,89 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using Emby.Dlna.Common;
using Emby.Dlna.Service;
+using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
- public class MediaReceiverRegistrarXmlBuilder
+ /// <summary>
+ /// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
+ /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
+ /// </summary>
+ public static class MediaReceiverRegistrarXmlBuilder
{
- public string GetXml()
+ /// <summary>
+ /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
+ /// </summary>
+ /// <returns>An XML representation of this service.</returns>
+ public static string GetXml()
{
- return new ServiceXmlBuilder().GetXml(
- new ServiceActionListBuilder().GetActions(),
- GetStateVariables());
+ return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
+ /// <summary>
+ /// The a list of all the state variables for this invocation.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
- var list = new List<StateVariable>();
-
- list.Add(new StateVariable
+ var list = new List<StateVariable>
{
- Name = "AuthorizationGrantedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "AuthorizationGrantedUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_DeviceID",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_DeviceID",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "AuthorizationDeniedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "AuthorizationDeniedUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "ValidationSucceededUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "ValidationSucceededUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RegistrationRespMsg",
- DataType = "bin.base64",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RegistrationRespMsg",
+ DataType = "bin.base64",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RegistrationReqMsg",
- DataType = "bin.base64",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RegistrationReqMsg",
+ DataType = "bin.base64",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "ValidationRevokedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "ValidationRevokedUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Result",
- DataType = "int",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Result",
+ DataType = "int",
+ SendsEvents = false
+ }
+ };
return list;
}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
index 13545c689..1dc9c79c1 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
@@ -1,13 +1,19 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using Emby.Dlna.Common;
+using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
- public class ServiceActionListBuilder
+ /// <summary>
+ /// Defines the <see cref="ServiceActionListBuilder" />.
+ /// </summary>
+ public static class ServiceActionListBuilder
{
- public IEnumerable<ServiceAction> GetActions()
+ /// <summary>
+ /// Returns a list of services that this instance provides.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
+ public static IEnumerable<ServiceAction> GetActions()
{
return new[]
{
@@ -21,6 +27,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
};
}
+ /// <summary>
+ /// Returns the action details for "IsValidated".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsValidated()
{
var action = new ServiceAction
@@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
+ /// <summary>
+ /// Returns the action details for "IsAuthorized".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsAuthorized()
{
var action = new ServiceAction
@@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
+ /// <summary>
+ /// Returns the action details for "RegisterDevice".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetRegisterDevice()
{
var action = new ServiceAction
@@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
+ /// <summary>
+ /// Returns the action details for "GetValidationSucceededUpdateID".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetValidationSucceededUpdateID()
{
var action = new ServiceAction
@@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
- private ServiceAction GetGetAuthorizationDeniedUpdateID()
+ /// <summary>
+ /// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetGetAuthorizationDeniedUpdateID()
{
var action = new ServiceAction
{
@@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
- private ServiceAction GetGetValidationRevokedUpdateID()
+ /// <summary>
+ /// Returns the action details for "GetValidationRevokedUpdateID".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetGetValidationRevokedUpdateID()
{
var action = new ServiceAction
{
@@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
- private ServiceAction GetGetAuthorizationGrantedUpdateID()
+ /// <summary>
+ /// Returns the action details for "GetAuthorizationGrantedUpdateID".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetGetAuthorizationGrantedUpdateID()
{
var action = new ServiceAction
{
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 63fd8ce5a..c07c8aefa 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
{
- _logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
+ _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
@@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
var startIndex = command.StartIndex ?? 0;
if (startIndex > 0)
{
- items = items.Skip(startIndex).ToList();
+ items = items.GetRange(startIndex, items.Count - startIndex);
}
var playlist = new List<PlaylistItem>();
@@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo
}
/// <inheritdoc />
- public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
+ public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
@@ -823,17 +823,17 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask;
}
- if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
+ if (name == SessionMessageType.Play)
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
- if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
+ if (name == SessionMessageType.PlayState)
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
- if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
+ if (name == SessionMessageType.GeneralCommand)
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index 21877f121..e93aef304 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -217,15 +217,15 @@ namespace Emby.Dlna.PlayTo
SupportedCommands = new[]
{
- GeneralCommandType.VolumeDown.ToString(),
- GeneralCommandType.VolumeUp.ToString(),
- GeneralCommandType.Mute.ToString(),
- GeneralCommandType.Unmute.ToString(),
- GeneralCommandType.ToggleMute.ToString(),
- GeneralCommandType.SetVolume.ToString(),
- GeneralCommandType.SetAudioStreamIndex.ToString(),
- GeneralCommandType.SetSubtitleStreamIndex.ToString(),
- GeneralCommandType.PlayMediaSource.ToString()
+ GeneralCommandType.VolumeDown,
+ GeneralCommandType.VolumeUp,
+ GeneralCommandType.Mute,
+ GeneralCommandType.Unmute,
+ GeneralCommandType.ToggleMute,
+ GeneralCommandType.SetVolume,
+ GeneralCommandType.SetAudioStreamIndex,
+ GeneralCommandType.SetSubtitleStreamIndex,
+ GeneralCommandType.PlayMediaSource
},
SupportsMediaControl = true
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 1f429d0de..bca9e81cd 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -235,13 +235,13 @@ namespace Emby.Dlna.Server
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
.Append("</serviceId>");
builder.Append("<SCPDURL>")
- .Append(BuildUrl(service.ScpdUrl, true))
+ .Append(BuildUrl(service.ScpdUrl))
.Append("</SCPDURL>");
builder.Append("<controlURL>")
- .Append(BuildUrl(service.ControlUrl, true))
+ .Append(BuildUrl(service.ControlUrl))
.Append("</controlURL>");
builder.Append("<eventSubURL>")
- .Append(BuildUrl(service.EventSubUrl, true))
+ .Append(BuildUrl(service.EventSubUrl))
.Append("</eventSubURL>");
builder.Append("</service>");
@@ -250,13 +250,7 @@ namespace Emby.Dlna.Server
builder.Append("</serviceList>");
}
- /// <summary>
- /// Builds a valid url for inclusion in the xml.
- /// </summary>
- /// <param name="url">Url to include.</param>
- /// <param name="absoluteUrl">Optional. When set to true, the absolute url is always used.</param>
- /// <returns>The url to use for the element.</returns>
- private string BuildUrl(string url, bool absoluteUrl = false)
+ private string BuildUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
@@ -267,7 +261,7 @@ namespace Emby.Dlna.Server
url = "/dlna/" + _serverUdn + "/" + url;
- if (EnableAbsoluteUrls || absoluteUrl)
+ if (EnableAbsoluteUrls)
{
url = _serverAddress.TrimEnd('/') + url;
}
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index d160e3339..198852ec1 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -60,10 +60,8 @@ namespace Emby.Dlna.Service
Async = true
};
- using (var reader = XmlReader.Create(streamReader, readerSettings))
- {
- requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
- }
+ using var reader = XmlReader.Create(streamReader, readerSettings);
+ requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
@@ -124,10 +122,8 @@ namespace Emby.Dlna.Service
{
if (!reader.IsEmptyElement)
{
- using (var subReader = reader.ReadSubtree())
- {
- return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
- }
+ using var subReader = reader.ReadSubtree();
+ return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
}
else
{
@@ -150,12 +146,12 @@ namespace Emby.Dlna.Service
}
}
- return new ControlRequestInfo();
+ throw new EndOfStreamException("Stream ended but no body tag found.");
}
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{
- var result = new ControlRequestInfo();
+ string namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false);
@@ -165,16 +161,15 @@ namespace Emby.Dlna.Service
{
if (reader.NodeType == XmlNodeType.Element)
{
- result.LocalName = reader.LocalName;
- result.NamespaceURI = reader.NamespaceURI;
+ localName = reader.LocalName;
+ namespaceURI = reader.NamespaceURI;
if (!reader.IsEmptyElement)
{
- using (var subReader = reader.ReadSubtree())
- {
- await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
- return result;
- }
+ var result = new ControlRequestInfo(localName, namespaceURI);
+ using var subReader = reader.ReadSubtree();
+ await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
+ return result;
}
else
{
@@ -187,7 +182,12 @@ namespace Emby.Dlna.Service
}
}
- return result;
+ if (localName != null && namespaceURI != null)
+ {
+ return new ControlRequestInfo(localName, namespaceURI);
+ }
+
+ throw new EndOfStreamException("Stream ended but no control found.");
}
private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
@@ -234,11 +234,18 @@ namespace Emby.Dlna.Service
private class ControlRequestInfo
{
+ public ControlRequestInfo(string localName, string namespaceUri)
+ {
+ LocalName = localName;
+ NamespaceURI = namespaceUri;
+ Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
- public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ public Dictionary<string, string> Headers { get; }
}
}
}
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index ed20292f6..8a2301d2d 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -36,7 +36,7 @@ namespace Emby.Drawing
private readonly IImageEncoder _imageEncoder;
private readonly IMediaEncoder _mediaEncoder;
- private bool _disposed = false;
+ private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ImageProcessor"/> class.
@@ -466,11 +466,11 @@ namespace Emby.Drawing
}
/// <inheritdoc />
- public void CreateImageCollage(ImageCollageOptions options)
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
_logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
- _imageEncoder.CreateImageCollage(options);
+ _imageEncoder.CreateImageCollage(options, libraryName);
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
}
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs
index bbb5c1716..2a1cfd3da 100644
--- a/Emby.Drawing/NullImageEncoder.cs
+++ b/Emby.Drawing/NullImageEncoder.cs
@@ -38,7 +38,7 @@ namespace Emby.Drawing
}
/// <inheritdoc />
- public void CreateImageCollage(ImageCollageOptions options)
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
throw new NotImplementedException();
}
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index ed53bd04f..5807d4688 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
_options = options;
}
- public AudioBookFileInfo ParseFile(string path)
+ public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
{
- return Resolve(path, false);
- }
-
- public AudioBookFileInfo ParseDirectory(string path)
- {
- return Resolve(path, true);
- }
-
- public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
- {
- if (string.IsNullOrEmpty(path))
+ if (path.Length == 0)
{
- throw new ArgumentNullException(nameof(path));
+ throw new ArgumentException("String can't be empty.", nameof(path));
}
// TODO
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index ded22d26c..7116d52b1 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -209,7 +209,10 @@ namespace Emby.Notifications
_libraryUpdateTimer = null;
}
- items = items.Take(10).ToList();
+ if (items.Count > 10)
+ {
+ items = items.GetRange(0, 10);
+ }
foreach (var item in items)
{
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 2adc1d6c3..660bbb2de 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
}
/// <inheritdoc />
- public string VirtualDataPath { get; } = "%AppDataPath%";
+ public string VirtualDataPath => "%AppDataPath%";
/// <summary>
/// Gets the image cache path.
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7a46fdf2e..9d5b651d9 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
@@ -97,6 +95,7 @@ using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
+using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Mvc;
@@ -127,7 +126,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private IHttpClientFactory _httpClientFactory;
- private IWebSocketManager _webSocketManager;
private string[] _urlPrefixes;
@@ -258,8 +256,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
- _jsonSerializer = new JsonSerializer();
-
+ _jsonSerializer = new JsonSerializer();
+
ServiceCollection = serviceCollection;
_networkManager = networkManager;
@@ -339,7 +337,7 @@ namespace Emby.Server.Implementations
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
- public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org";
+ public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary>
/// Gets the current application name.
@@ -403,7 +401,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Resolves this instance.
/// </summary>
- /// <typeparam name="T">The type</typeparam>
+ /// <typeparam name="T">The type.</typeparam>
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
@@ -537,6 +535,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>();
+ ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(_networkManager);
@@ -665,7 +664,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
_httpClientFactory = Resolve<IHttpClientFactory>();
- _webSocketManager = Resolve<IWebSocketManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@@ -786,7 +784,6 @@ namespace Emby.Server.Implementations
.ToArray();
_urlPrefixes = GetUrlPrefixes().ToArray();
- _webSocketManager.Init(GetExports<IWebSocketListener>());
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
@@ -819,38 +816,6 @@ namespace Emby.Server.Implementations
{
try
{
- if (plugin is IPluginAssembly assemblyPlugin)
- {
- var assembly = plugin.GetType().Assembly;
- var assemblyName = assembly.GetName();
- var assemblyFilePath = assembly.Location;
-
- var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
-
- assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
-
- try
- {
- var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
- if (idAttributes.Length > 0)
- {
- var attribute = (GuidAttribute)idAttributes[0];
- var assemblyId = new Guid(attribute.Value);
-
- assemblyPlugin.SetId(assemblyId);
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName);
- }
- }
-
- if (plugin is IHasPluginConfiguration hasPluginConfiguration)
- {
- hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
- }
-
plugin.RegisterServices(ServiceCollection);
}
catch (Exception ex)
@@ -1026,80 +991,54 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
- /// <summary>
- /// Comparison function used in <see cref="GetPlugins" />.
- /// </summary>
- /// <param name="a">Item to compare.</param>
- /// <param name="b">Item to compare with.</param>
- /// <returns>Boolean result of the operation.</returns>
- private static int VersionCompare(
- (Version PluginVersion, string Name, string Path) a,
- (Version PluginVersion, string Name, string Path) b)
- {
- int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
-
- if (compare == 0)
- {
- return a.PluginVersion.CompareTo(b.PluginVersion);
- }
-
- return compare;
- }
-
- /// <summary>
- /// Returns a list of plugins to install.
- /// </summary>
- /// <param name="path">Path to check.</param>
- /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
- /// <returns>Enumerable list of dlls to load.</returns>
- private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+ /// <inheritdoc/>
+ public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
{
- var dllList = new List<string>();
- var versions = new List<(Version PluginVersion, string Name, string Path)>();
+ var minimumVersion = new Version(0, 0, 0, 1);
+ var versions = new List<LocalPlugin>();
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
- string metafile;
foreach (var dir in directories)
{
try
{
- metafile = Path.Combine(dir, "meta.json");
+ var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
- targetAbi = new Version(0, 0, 0, 1);
+ targetAbi = minimumVersion;
}
if (!Version.TryParse(manifest.Version, out var version))
{
- version = new Version(0, 0, 0, 1);
+ version = minimumVersion;
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
- versions.Add((version, manifest.Name, dir));
+ versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
}
}
else
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
-
+
int versionIndex = dir.LastIndexOf('_');
- if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+ if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
{
// Versioned folder.
- versions.Add((ver, metafile, dir));
+ versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
- // Un-versioned folder - Add it under the path name and version 0.0.0.1.
- versions.Add((new Version(0, 0, 0, 1), metafile, dir));
- }
+ // Un-versioned folder - Add it under the path name and version 0.0.0.1.
+ versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
+ }
}
}
catch
@@ -1109,14 +1048,14 @@ namespace Emby.Server.Implementations
}
string lastName = string.Empty;
- versions.Sort(VersionCompare);
+ versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
- dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+ versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
@@ -1124,6 +1063,7 @@ namespace Emby.Server.Implementations
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
+ versions.RemoveAt(x);
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
@@ -1136,7 +1076,7 @@ namespace Emby.Server.Implementations
}
}
- return dllList;
+ return versions;
}
/// <summary>
@@ -1147,21 +1087,24 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
- foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
+ foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
{
- Assembly plugAss;
- try
- {
- plugAss = Assembly.LoadFrom(file);
- }
- catch (FileLoadException ex)
+ foreach (var file in plugin.DllFiles)
{
- Logger.LogError(ex, "Failed to load assembly {Path}", file);
- continue;
- }
+ Assembly plugAss;
+ try
+ {
+ plugAss = Assembly.LoadFrom(file);
+ }
+ catch (FileLoadException ex)
+ {
+ Logger.LogError(ex, "Failed to load assembly {Path}", file);
+ continue;
+ }
- Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
- yield return plugAss;
+ Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
+ yield return plugAss;
+ }
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index fb1bb65a0..19045b72b 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
var all = channels;
var totalCount = all.Count;
- if (query.StartIndex.HasValue)
+ if (query.StartIndex.HasValue || query.Limit.HasValue)
{
- all = all.Skip(query.StartIndex.Value).ToList();
+ int startIndex = query.StartIndex ?? 0;
+ int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
+ all = all.GetRange(startIndex, count);
}
- if (query.Limit.HasValue)
- {
- all = all.Take(query.Limit.Value).ToList();
- }
-
- var returnItems = all.ToArray();
-
if (query.RefreshLatestChannelItems)
{
- foreach (var item in returnItems)
+ foreach (var item in all)
{
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
}
@@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
return new QueryResult<Channel>
{
- Items = returnItems,
+ Items = all,
TotalRecordCount = totalCount
};
}
@@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds(
new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Channel).Name },
+ IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
}
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
index eeb49b8fe..2391eed42 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Channel).Name },
+ IncludeItemTypes = new[] { nameof(Channel) },
ExcludeItemIds = installedChannelIds.ToArray()
});
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 0fb050a7a..8c756a7f4 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data
protected bool TableExists(ManagedConnection connection, string name)
{
- return connection.RunInTransaction(db =>
+ return connection.RunInTransaction(
+ db =>
{
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index d09f84e17..acb75e9b8 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
{
connection.RunQueries(queries);
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var existingColumnNames = GetColumnNames(db, "AncestorIds");
AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
@@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
SaveItemsInTranscation(db, tuples);
}, TransactionMode);
@@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
// First delete chapters
db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
@@ -2921,7 +2925,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<BaseItem>();
using (var connection = GetConnection(true))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var statements = PrepareAll(db, statementTexts);
@@ -3324,7 +3329,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<Guid>();
using (var connection = GetConnection(true))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var statements = PrepareAll(db, statementTexts);
@@ -3908,7 +3914,7 @@ namespace Emby.Server.Implementations.Data
if (query.IsPlayed.HasValue)
{
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
- if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
{
if (query.IsPlayed.Value)
{
@@ -4749,29 +4755,29 @@ namespace Emby.Server.Implementations.Data
{
var list = new List<string>();
- if (IsTypeInQuery(typeof(Person).Name, query))
+ if (IsTypeInQuery(nameof(Person), query))
{
- list.Add(typeof(Person).Name);
+ list.Add(nameof(Person));
}
- if (IsTypeInQuery(typeof(Genre).Name, query))
+ if (IsTypeInQuery(nameof(Genre), query))
{
- list.Add(typeof(Genre).Name);
+ list.Add(nameof(Genre));
}
- if (IsTypeInQuery(typeof(MusicGenre).Name, query))
+ if (IsTypeInQuery(nameof(MusicGenre), query))
{
- list.Add(typeof(MusicGenre).Name);
+ list.Add(nameof(MusicGenre));
}
- if (IsTypeInQuery(typeof(MusicArtist).Name, query))
+ if (IsTypeInQuery(nameof(MusicArtist), query))
{
- list.Add(typeof(MusicArtist).Name);
+ list.Add(nameof(MusicArtist));
}
- if (IsTypeInQuery(typeof(Studio).Name, query))
+ if (IsTypeInQuery(nameof(Studio), query))
{
- list.Add(typeof(Studio).Name);
+ list.Add(nameof(Studio));
}
return list;
@@ -4826,12 +4832,12 @@ namespace Emby.Server.Implementations.Data
var types = new[]
{
- typeof(Episode).Name,
- typeof(Video).Name,
- typeof(Movie).Name,
- typeof(MusicVideo).Name,
- typeof(Series).Name,
- typeof(Season).Name
+ nameof(Episode),
+ nameof(Video),
+ nameof(Movie),
+ nameof(MusicVideo),
+ nameof(Series),
+ nameof(Season)
};
if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
@@ -4899,7 +4905,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
connection.ExecuteAll(sql);
}, TransactionMode);
@@ -4950,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var idBlob = id.ToByteArray();
@@ -4994,26 +5002,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- var commandText = "select Distinct Name from People";
+ var commandText = new StringBuilder("select Distinct p.Name from People p");
+
+ if (query.User != null && query.IsFavorite.HasValue)
+ {
+ commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
+ commandText.Append(typeof(Person).FullName);
+ commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
+ }
var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
}
- commandText += " order by ListOrder";
+ commandText.Append(" order by ListOrder");
if (query.Limit > 0)
{
- commandText += " LIMIT " + query.Limit;
+ commandText.Append(" LIMIT ").Append(query.Limit);
}
using (var connection = GetConnection(true))
{
var list = new List<string>();
- using (var statement = PrepareStatement(connection, commandText))
+ using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
@@ -5079,19 +5094,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (!query.ItemId.Equals(Guid.Empty))
{
whereClauses.Add("ItemId=@ItemId");
- if (statement != null)
- {
- statement.TryBind("@ItemId", query.ItemId.ToByteArray());
- }
+ statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
}
if (!query.AppearsInItemId.Equals(Guid.Empty))
{
- whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
- if (statement != null)
- {
- statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
- }
+ whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
+ statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@@ -5099,10 +5108,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryPersonTypes.Count == 1)
{
whereClauses.Add("PersonType=@PersonType");
- if (statement != null)
- {
- statement.TryBind("@PersonType", queryPersonTypes[0]);
- }
+ statement?.TryBind("@PersonType", queryPersonTypes[0]);
}
else if (queryPersonTypes.Count > 1)
{
@@ -5116,10 +5122,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryExcludePersonTypes.Count == 1)
{
whereClauses.Add("PersonType<>@PersonType");
- if (statement != null)
- {
- statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
- }
+ statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
else if (queryExcludePersonTypes.Count > 1)
{
@@ -5131,19 +5134,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (query.MaxListOrder.HasValue)
{
whereClauses.Add("ListOrder<=@MaxListOrder");
- if (statement != null)
- {
- statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
- }
+ statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
if (!string.IsNullOrWhiteSpace(query.NameContains))
{
- whereClauses.Add("Name like @NameContains");
- if (statement != null)
- {
- statement.TryBind("@NameContains", "%" + query.NameContains + "%");
- }
+ whereClauses.Add("p.Name like @NameContains");
+ statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
+ }
+
+ if (query.IsFavorite.HasValue)
+ {
+ whereClauses.Add("isFavorite=@IsFavorite");
+ statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
+ }
+
+ if (query.User != null)
+ {
+ statement?.TryBind("@UserId", query.User.InternalId);
}
return whereClauses;
@@ -5357,7 +5365,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
itemCountColumns = new Dictionary<string, string>()
{
- { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"}
+ { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
};
}
@@ -5412,6 +5420,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
Tags = query.Tags,
OfficialRatings = query.OfficialRatings,
+ StudioIds = query.StudioIds,
GenreIds = query.GenreIds,
Genres = query.Genres,
Years = query.Years,
@@ -5744,7 +5753,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = itemId.ToByteArray();
@@ -5898,7 +5908,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = id.ToByteArray();
@@ -6232,7 +6243,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = id.ToByteArray();
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 4a78aac8e..2c4e8e0fc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data
var users = userDatasTableExists ? null : userManager.Users;
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
db.ExecuteAll(string.Join(";", new[] {
@@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
SaveUserData(db, internalUserId, key, userData);
}, TransactionMode);
@@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
foreach (var userItemData in userDataList)
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index edb8753fd..73502c2c9 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -465,7 +465,7 @@ namespace Emby.Server.Implementations.Dto
{
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Name = item.Album,
Limit = 1
});
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 9ed3cca99..c762aa0b8 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -32,10 +32,10 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
<PackageReference Include="Mono.Nat" Version="3.0.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 2e9d638ec..ae1b51b4c 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -17,6 +17,7 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@@ -106,7 +107,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- _sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None);
+ _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
}
catch
{
@@ -124,7 +125,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- _sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None);
+ _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
}
catch
{
@@ -348,7 +349,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false);
+ await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index 44d2580d6..824bb85f4 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints
private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
- await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
+ await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
- await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
+ await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
- await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
+ await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
- await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
+ await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
}
- private async Task SendMessage(string name, TimerEventInfo info)
+ private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
{
var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 1da717e75..1989e9ed2 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
{
- return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
+ return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 68d981ad1..df7a034e8 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
- if (auth?.User == null)
+ if (!auth.IsAuthenticated)
{
- return null;
+ throw new AuthenticationException("Invalid token.");
}
- if (auth.User.HasPermission(PermissionKind.IsDisabled))
+ if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
{
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 4b407dd9d..e733c9092 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
- var (authInfo, _) =
- GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+ var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
@@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
- var (authInfo, originalAuthInfo) =
- GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
-
- if (originalAuthInfo != null)
- {
- httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
- }
+ var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
- private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+ private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
@@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
Device = device,
DeviceId = deviceId,
Version = version,
- Token = token
+ Token = token,
+ IsAuthenticated = false
};
- AuthenticationInfo originalAuthenticationInfo = null;
- if (!string.IsNullOrWhiteSpace(token))
+ if (string.IsNullOrWhiteSpace(token))
{
- var result = _authRepo.Get(new AuthenticationInfoQuery
- {
- AccessToken = token
- });
+ // Request doesn't contain a token.
+ return authInfo;
+ }
- originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
+ var result = _authRepo.Get(new AuthenticationInfoQuery
+ {
+ AccessToken = token
+ });
- if (originalAuthenticationInfo != null)
- {
- var updateToken = false;
+ if (result.Items.Count > 0)
+ {
+ authInfo.IsAuthenticated = true;
+ }
- // TODO: Remove these checks for IsNullOrWhiteSpace
- if (string.IsNullOrWhiteSpace(authInfo.Client))
- {
- authInfo.Client = originalAuthenticationInfo.AppName;
- }
+ var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
- if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
- {
- authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
- }
+ if (originalAuthenticationInfo != null)
+ {
+ var updateToken = false;
- // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+ // TODO: Remove these checks for IsNullOrWhiteSpace
+ if (string.IsNullOrWhiteSpace(authInfo.Client))
+ {
+ authInfo.Client = originalAuthenticationInfo.AppName;
+ }
- if (string.IsNullOrWhiteSpace(authInfo.Device))
- {
- authInfo.Device = originalAuthenticationInfo.DeviceName;
- }
- else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
- {
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- originalAuthenticationInfo.DeviceName = authInfo.Device;
- }
- }
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+ {
+ authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
+ }
- if (string.IsNullOrWhiteSpace(authInfo.Version))
- {
- authInfo.Version = originalAuthenticationInfo.AppVersion;
- }
- else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
+ var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
+ {
+ authInfo.Device = originalAuthenticationInfo.DeviceName;
+ }
+ else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
{
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- originalAuthenticationInfo.AppVersion = authInfo.Version;
- }
+ updateToken = true;
+ originalAuthenticationInfo.DeviceName = authInfo.Device;
}
+ }
- if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
+ {
+ authInfo.Version = originalAuthenticationInfo.AppVersion;
+ }
+ else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
{
- originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
+ originalAuthenticationInfo.AppVersion = authInfo.Version;
}
+ }
- if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
- {
- authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
+ if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
+ {
+ originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
+ updateToken = true;
+ }
- if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
- {
- originalAuthenticationInfo.UserName = authInfo.User.Username;
- updateToken = true;
- }
- }
+ if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
+ {
+ authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
- if (updateToken)
+ if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
- _authRepo.Update(originalAuthenticationInfo);
+ originalAuthenticationInfo.UserName = authInfo.User.Username;
+ updateToken = true;
}
+
+ authInfo.IsApiKey = true;
+ }
+ else
+ {
+ authInfo.IsApiKey = false;
+ }
+
+ if (updateToken)
+ {
+ _authRepo.Update(originalAuthenticationInfo);
}
}
- return (authInfo, originalAuthenticationInfo);
+ return authInfo;
}
/// <summary>
@@ -267,7 +274,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (param.Length == 2)
{
var value = NormalizeValue(param[1].Trim(new[] { '"' }));
- result.Add(param[0], value);
+ result[param[0]] = value;
}
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7eae4e764..fed2addf8 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer
Connection = this
};
- if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+ if (info.MessageType == SessionMessageType.KeepAlive)
{
await SendKeepAliveResponse().ConfigureAwait(false);
}
@@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
new WebSocketMessage<string>
{
MessageId = Guid.NewGuid(),
- MessageType = "KeepAlive"
+ MessageType = SessionMessageType.KeepAlive
}, CancellationToken.None);
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 89c1b7ea0..71ece80a7 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
@@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
{
public class WebSocketManager : IWebSocketManager
{
+ private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory;
- private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private bool _disposed = false;
public WebSocketManager(
+ Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory)
{
+ _webSocketListeners = webSocketListeners;
_logger = logger;
_loggerFactory = loggerFactory;
}
@@ -69,15 +70,6 @@ namespace Emby.Server.Implementations.HttpServer
}
/// <summary>
- /// Adds the rest handlers.
- /// </summary>
- /// <param name="listeners">The web socket listeners.</param>
- public void Init(IEnumerable<IWebSocketListener> listeners)
- {
- _webSocketListeners = listeners.ToArray();
- }
-
- /// <summary>
/// Processes the web socket message received.
/// </summary>
/// <param name="result">The result.</param>
@@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
IEnumerable<Task> GetTasks()
{
- foreach (var x in _webSocketListeners)
+ var listeners = _webSocketListeners.Value;
+ foreach (var x in listeners)
{
yield return x.ProcessMessageAsync(result);
}
diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
index bf57382ed..afa4ec7b1 100644
--- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
// return _libraryManager.GetItemList(new InternalItemsQuery
// {
// ArtistIds = new[] { item.Id },
- // IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ // IncludeItemTypes = new[] { nameof(MusicAlbum) },
// OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
// Limit = 4,
// Recursive = true,
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 57302b506..5f7e51858 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
{
+ var useBackdrop = primaryItem is CollectionFolder;
return items
.Select(i =>
{
+ // Use Backdrop instead of Primary image for Library images.
+ if (useBackdrop)
+ {
+ var backdrop = i.GetImageInfo(ImageType.Backdrop, 0);
+ if (backdrop != null && backdrop.IsLocalFile)
+ {
+ return backdrop.Path;
+ }
+ }
+
var image = i.GetImageInfo(ImageType.Primary, 0);
if (image != null && image.IsLocalFile)
{
@@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images
return null;
}
- ImageProcessor.CreateImageCollage(options);
+ ImageProcessor.CreateImageCollage(options, primaryItem.Name);
return outputPath;
}
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 1cd4cd66b..381788231 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name },
+ IncludeItemTypes = new[]
+ {
+ nameof(MusicAlbum),
+ nameof(MusicVideo),
+ nameof(Audio)
+ },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
@@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
- IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
+ IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 00282b71a..f16eda1ec 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
}
+ public BaseItem GetParentItem(string parentId, Guid? userId)
+ {
+ if (!string.IsNullOrEmpty(parentId))
+ {
+ return GetItemById(new Guid(parentId));
+ }
+
+ if (userId.HasValue && userId != Guid.Empty)
+ {
+ return GetUserRootFolder();
+ }
+
+ return RootFolder;
+ }
+
/// <inheritdoc />
public bool IsVideoFile(string path)
{
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 67cf8bf5b..376a15570 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
- private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private IMediaSourceProvider[] _providers;
@@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
- public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
+ public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ var info = _openStreams.Values.FirstOrDefault(i =>
{
- var info = _openStreams.Values.FirstOrDefault(i =>
+ var liveStream = i as ILiveStream;
+ if (liveStream != null)
{
- var liveStream = i as ILiveStream;
- if (liveStream != null)
- {
- return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
- }
+ return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
+ }
- return false;
- });
+ return false;
+ });
- return info as IDirectStreamProvider;
- }
- finally
- {
- _liveStreamSemaphore.Release();
- }
+ return Task.FromResult(info as IDirectStreamProvider);
}
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
}
- private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ if (_openStreams.TryGetValue(id, out ILiveStream info))
{
- if (_openStreams.TryGetValue(id, out ILiveStream info))
- {
- return info;
- }
- else
- {
- throw new ResourceNotFoundException();
- }
+ return Task.FromResult(info);
}
- finally
+ else
{
- _liveStreamSemaphore.Release();
+ return Task.FromException<ILiveStream>(new ResourceNotFoundException());
}
}
@@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library
if (liveStream.ConsumerCount <= 0)
{
- _openStreams.Remove(id);
+ _openStreams.TryRemove(id, out _);
_logger.LogInformation("Closing live stream {0}", id);
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 877fdec86..658c53f28 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
DtoOptions = dtoOptions
})
.Cast<Audio>()
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = genreIds.ToArray(),
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 03059e6d3..70be52411 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth;
- public MultiItemResolverResult ResolveMultiple(Folder parent,
+ public MultiItemResolverResult ResolveMultiple(
+ Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService)
@@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return result;
}
- private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
+ private MultiItemResolverResult ResolveMultipleInternal(
+ Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 79b6dded3..18ceb5e76 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using Emby.Naming.Audio;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -113,52 +116,48 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
+ // check for audio files before digging down into directories
+ var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
+ if (foundAudioFile)
+ {
+ // at least one audio file exists
+ return true;
+ }
+
+ if (!allowSubfolders)
+ {
+ // not music since no audio file exists and we're not looking into subfolders
+ return false;
+ }
+
var discSubfolderCount = 0;
- var notMultiDisc = false;
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var parser = new AlbumParser(namingOptions);
- foreach (var fileSystemInfo in list)
+
+ var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
+
+ var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{
- if (fileSystemInfo.IsDirectory)
+ var path = fileSystemInfo.FullName;
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
+
+ if (hasMusic)
{
- if (allowSubfolders)
+ if (parser.IsMultiPart(path))
{
- if (notMultiDisc)
- {
- continue;
- }
-
- var path = fileSystemInfo.FullName;
- var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
-
- if (hasMusic)
- {
- if (parser.IsMultiPart(path))
- {
- logger.LogDebug("Found multi-disc folder: " + path);
- discSubfolderCount++;
- }
- else
- {
- // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
- notMultiDisc = true;
- }
- }
+ logger.LogDebug("Found multi-disc folder: " + path);
+ Interlocked.Increment(ref discSubfolderCount);
}
- }
- else
- {
- var fullName = fileSystemInfo.FullName;
-
- if (libraryManager.IsAudioFile(fullName))
+ else
{
- return true;
+ // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
+ state.Stop();
}
}
- }
+ });
- if (notMultiDisc)
+ if (!result.IsCompleted)
{
return false;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 5f5cd0e92..e9e688fa6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
// If we contain an album assume we are an artist folder
- return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
+ var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
+
+ var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
+ {
+ if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+ {
+ // stop once we see a music album
+ state.Stop();
+ }
+ });
+
+ return !result.IsCompleted ? new MusicArtist() : null;
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 86a5d8b7d..59af7ce8a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var fileExtension = Path.GetExtension(f.FullName) ??
string.Empty;
- return _validExtensions.Contains(fileExtension,
+ return _validExtensions.Contains(
+ fileExtension,
StringComparer
.OrdinalIgnoreCase);
}).ToList();
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 9a69bce0e..c850e3a08 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = query.ExcludeItemTypes.ToList();
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
- excludeItemTypes.Add(typeof(Year).Name);
- excludeItemTypes.Add(typeof(Folder).Name);
+ excludeItemTypes.Add(nameof(Year));
+ excludeItemTypes.Add(nameof(Folder));
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(Genre).Name);
- AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
+ AddIfMissing(includeItemTypes, nameof(Genre));
+ AddIfMissing(includeItemTypes, nameof(MusicGenre));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(Genre).Name);
- AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
+ AddIfMissing(excludeItemTypes, nameof(Genre));
+ AddIfMissing(excludeItemTypes, nameof(MusicGenre));
}
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(Person).Name);
+ AddIfMissing(includeItemTypes, nameof(Person));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(Person).Name);
+ AddIfMissing(excludeItemTypes, nameof(Person));
}
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(Studio).Name);
+ AddIfMissing(includeItemTypes, nameof(Studio));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(Studio).Name);
+ AddIfMissing(excludeItemTypes, nameof(Studio));
}
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
+ AddIfMissing(includeItemTypes, nameof(MusicArtist));
}
}
else
{
- AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
+ AddIfMissing(excludeItemTypes, nameof(MusicArtist));
}
- AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
- AddIfMissing(excludeItemTypes, typeof(Folder).Name);
+ AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
+ AddIfMissing(excludeItemTypes, nameof(Folder));
var mediaTypes = query.MediaTypes.ToList();
if (includeItemTypes.Count > 0)
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index d4c8c35e6..f9a3e2c64 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(MusicArtist).Name },
+ IncludeItemTypes = new[] { nameof(MusicArtist) },
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 8275c873a..8739a9e1b 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Person).Name },
+ IncludeItemTypes = new[] { nameof(Person) },
IsDeadPerson = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index ca35adfff..9a8c5f39d 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Studio).Name },
+ IncludeItemTypes = new[] { nameof(Studio) },
IsDeadStudio = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index ca60c3366..fcc2d1eeb 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
Limit = 1,
ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true)
@@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
Limit = 1,
DtoOptions = new DtoOptions(true)
{
@@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true)
{
@@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { parent.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { programInfo.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var seriesIds = _libraryManager.GetItemIds(
new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Series).Name },
+ IncludeItemTypes = new[] { nameof(Series) },
Name = program.Name
}).ToArray();
@@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var result = _libraryManager.GetItemIds(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds,
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 49ad73af3..6af49dd45 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
@@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
{
program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = programSeriesId,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 5bdd1c23c..9c7d624ee 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
IsKids = query.IsKids,
IsSports = query.IsSports,
IsSeries = query.IsSeries,
- IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvChannel) },
TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked,
@@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate,
@@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
IsAiring = query.IsAiring,
HasAired = query.HasAired,
IsNews = query.IsNews,
@@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
if (cleanDatabase)
{
- CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
- CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
+ CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
+ CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
}
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsMovie.Value)
{
- includeItemTypes.Add(typeof(Movie).Name);
+ includeItemTypes.Add(nameof(Movie));
}
else
{
- excludeItemTypes.Add(typeof(Movie).Name);
+ excludeItemTypes.Add(nameof(Movie));
}
}
@@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsSeries.Value)
{
- includeItemTypes.Add(typeof(Episode).Name);
+ includeItemTypes.Add(nameof(Episode));
}
else
{
- excludeItemTypes.Add(typeof(Episode).Name);
+ excludeItemTypes.Add(nameof(Episode));
}
}
@@ -1883,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
ChannelIds = channelIds,
MaxStartDate = now,
MinEndDate = now,
diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
index f1b61f7c7..582b64923 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
@@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 6730751d5..858c10030 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false);
}
+ public string GetFilePath()
+ {
+ return TempFilePath;
+ }
+
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 8107bc427..4b170b2e4 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
- return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
+ return await new M3uParser(Logger, _httpClientFactory, _appHost)
+ .Parse(info, channelIdPrefix, cancellationToken)
+ .ConfigureAwait(false);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index f066a749e..c064e2fe6 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
@@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_appHost = appHost;
}
- public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
+ public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
{
// Read the file and display it line by line.
- using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
+ using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{
- return GetChannels(reader, channelIdPrefix, tunerHostId);
+ return GetChannels(reader, channelIdPrefix, info.Id);
}
}
@@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
- public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
+ public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
{
- if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- return _httpClientFactory.CreateClient(NamedClient.Default)
- .GetStreamAsync(url);
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
+ if (!string.IsNullOrEmpty(info.UserAgent))
+ {
+ requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
+ }
+
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(requestMessage, cancellationToken)
+ .ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
- return Task.FromResult((Stream)File.OpenRead(url));
+ return File.OpenRead(info.Url);
}
private const string ExtInfPrefix = "#EXTINF:";
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 6c10fca8c..2e1b89509 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ // Response stream is disposed manually.
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
@@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
+ public string GetFilePath()
+ {
+ return TempFilePath;
+ }
+
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index d33e11893..977a1c2d7 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -85,7 +85,6 @@
"ItemAddedWithName": "{0} is in die versameling",
"HomeVideos": "Tuis opnames",
"HeaderRecordingGroups": "Groep Opnames",
- "HeaderCameraUploads": "Kamera Oplaai",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk",
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 4eac8e75d..4b898e6fe 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -16,7 +16,6 @@
"Folders": "المجلدات",
"Genres": "التضنيفات",
"HeaderAlbumArtists": "فناني الألبومات",
- "HeaderCameraUploads": "تحميلات الكاميرا",
"HeaderContinueWatching": "استئناف",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 3fc7c7dc0..1fed83276 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -16,7 +16,6 @@
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми",
- "HeaderCameraUploads": "Качени от камера",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 1bd190982..5667bf337 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -14,7 +14,6 @@
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
- "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
"HeaderAlbumArtists": "এলবাম শিল্পী",
"Genres": "জেনার",
"Folders": "ফোল্ডারগুলো",
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 2c802a39e..b7852eccb 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -16,7 +16,6 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes del Àlbum",
- "HeaderCameraUploads": "Pujades de Càmera",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Preferits",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 464ca28ca..fb31b01ff 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -16,7 +16,6 @@
"Folders": "Složky",
"Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba",
- "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti",
@@ -114,5 +113,7 @@
"TasksChannelsCategory": "Internetové kanály",
"TasksApplicationCategory": "Aplikace",
"TasksLibraryCategory": "Knihovna",
- "TasksMaintenanceCategory": "Údržba"
+ "TasksMaintenanceCategory": "Údržba",
+ "TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
+ "TaskCleanActivityLog": "Smazat záznam aktivity"
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index f5397b62c..b29ad94ef 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -16,7 +16,6 @@
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere",
- "HeaderCameraUploads": "Kamera Uploads",
"HeaderContinueWatching": "Fortsæt Afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index fcbe9566e..c81de8218 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -16,7 +16,6 @@
"Folders": "Verzeichnisse",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten",
- "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "Fortsetzen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten",
@@ -114,5 +113,7 @@
"TasksChannelsCategory": "Internet Kanäle",
"TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek",
- "TasksMaintenanceCategory": "Wartung"
+ "TasksMaintenanceCategory": "Wartung",
+ "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
+ "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 0753ea39d..c45cc11cb 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -16,7 +16,6 @@
"Folders": "Φάκελοι",
"Genres": "Είδη",
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
- "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 544c38cfa..57ff13219 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 97a843160..6d8b222b4 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
@@ -96,6 +95,8 @@
"TasksLibraryCategory": "Library",
"TasksApplicationCategory": "Application",
"TasksChannelsCategory": "Internet Channels",
+ "TaskCleanActivityLog": "Clean Activity Log",
+ "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanCache": "Clean Cache Directory",
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images",
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index ac96c788c..390074cdd 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas de álbum",
- "HeaderCameraUploads": "Subidas de cámara",
"HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 4ba324aa1..ab54c0ea6 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index e7bd3959b..60abc08d4 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
@@ -78,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
- "TvShows": "Programas de televisión",
+ "TvShows": "Series",
"User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index 0959ef2ca..dcd30694f 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -105,7 +105,6 @@
"Inherit": "Heredar",
"HomeVideos": "Videos caseros",
"HeaderRecordingGroups": "Grupos de grabación",
- "HeaderCameraUploads": "Subidas desde la cámara",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
"DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} se ha desconectado",
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index 26732eb3f..b64ffbfbb 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -12,7 +12,6 @@
"Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo",
- "HeaderCameraUploads": "Subidas de Cámara",
"HeaderAlbumArtists": "Artistas del Álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 500c29217..1986decf0 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -16,7 +16,6 @@
"Folders": "پوشه‌ها",
"Genres": "ژانرها",
"HeaderAlbumArtists": "هنرمندان آلبوم",
- "HeaderCameraUploads": "آپلودهای دوربین",
"HeaderContinueWatching": "ادامه تماشا",
"HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه",
"HeaderFavoriteArtists": "هنرمندان مورد علاقه",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index f8d6e0e09..8e219a9ce 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -1,7 +1,7 @@
{
"HeaderLiveTV": "Live-TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
- "NameSeasonUnknown": "Tuntematon Kausi",
+ "NameSeasonUnknown": "Tuntematon kausi",
"NameSeasonNumber": "Kausi {0}",
"NameInstallFailed": "{0} asennus epäonnistui",
"MusicVideos": "Musiikkivideot",
@@ -19,24 +19,23 @@
"ItemAddedWithName": "{0} lisättiin kirjastoon",
"Inherit": "Periytyä",
"HomeVideos": "Kotivideot",
- "HeaderRecordingGroups": "Nauhoiteryhmät",
+ "HeaderRecordingGroups": "Tallennusryhmät",
"HeaderNextUp": "Seuraavaksi",
- "HeaderFavoriteSongs": "Lempikappaleet",
- "HeaderFavoriteShows": "Lempisarjat",
- "HeaderFavoriteEpisodes": "Lempijaksot",
- "HeaderCameraUploads": "Kamerasta Lähetetyt",
- "HeaderFavoriteArtists": "Lempiartistit",
- "HeaderFavoriteAlbums": "Lempialbumit",
+ "HeaderFavoriteSongs": "Suosikkikappaleet",
+ "HeaderFavoriteShows": "Suosikkisarjat",
+ "HeaderFavoriteEpisodes": "Suosikkijaksot",
+ "HeaderFavoriteArtists": "Suosikkiartistit",
+ "HeaderFavoriteAlbums": "Suosikkialbumit",
"HeaderContinueWatching": "Jatka katsomista",
- "HeaderAlbumArtists": "Albumin esittäjä",
+ "HeaderAlbumArtists": "Albumin artistit",
"Genres": "Tyylilajit",
"Folders": "Kansiot",
"Favorites": "Suosikit",
"FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
"DeviceOnlineWithName": "{0} on yhdistetty",
- "DeviceOfflineWithName": "{0} on katkaissut yhteytensä",
+ "DeviceOfflineWithName": "{0} yhteys on katkaistu",
"Collections": "Kokoelmat",
- "ChapterNameValue": "Luku: {0}",
+ "ChapterNameValue": "Jakso: {0}",
"Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
"Books": "Kirjat",
@@ -62,25 +61,25 @@
"UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
"UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
"UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
- "UserOfflineFromDevice": "{0} yhteys katkaistu {1}",
+ "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
"UserLockedOutWithName": "Käyttäjä {0} lukittu",
"UserDownloadingItemWithValues": "{0} lataa {1}",
"UserDeletedWithName": "Käyttäjä {0} poistettu",
"UserCreatedWithName": "Käyttäjä {0} luotu",
- "TvShows": "TV-sarjat",
+ "TvShows": "TV-ohjelmat",
"Sync": "Synkronoi",
- "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry",
- "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.",
+ "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
+ "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
"Songs": "Kappaleet",
- "Shows": "Sarjat",
- "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
+ "Shows": "Ohjelmat",
+ "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
"ProviderValue": "Tarjoaja: {0}",
"Plugin": "Liitännäinen",
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
"NotificationOptionVideoPlayback": "Videota toistetaan",
"NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
- "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen",
+ "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
"NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
"NotificationOptionPluginInstalled": "Liitännäinen asennettu",
@@ -105,10 +104,10 @@
"TaskRefreshPeople": "Päivitä henkilöt",
"TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
"TaskCleanLogs": "Puhdista lokihakemisto",
- "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.",
+ "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
"TaskRefreshLibrary": "Skannaa mediakirjasto",
- "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.",
- "TaskRefreshChapterImages": "Eristä lukujen kuvat",
+ "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
+ "TaskRefreshChapterImages": "Pura jakson kuvat",
"TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
"TasksChannelsCategory": "Internet kanavat",
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 47daf2044..1a3e18832 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -73,7 +73,6 @@
"HeaderFavoriteArtists": "Paboritong Artista",
"HeaderFavoriteAlbums": "Paboritong Albums",
"HeaderContinueWatching": "Ituloy Manood",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderAlbumArtists": "Artista ng Album",
"Genres": "Kategorya",
"Folders": "Folders",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index cd1c8144f..3d7592e3c 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -16,7 +16,6 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
- "HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes favoris",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 7fc996821..cc9243f37 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -16,7 +16,6 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes",
- "HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
@@ -114,5 +113,7 @@
"TaskCleanCache": "Vider le répertoire cache",
"TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Bibliothèque",
- "TasksMaintenanceCategory": "Maintenance"
+ "TasksMaintenanceCategory": "Maintenance",
+ "TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
+ "TaskCleanActivityLog": "Nettoyer le journal d'activité"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index 8780a884b..ee1f8775e 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -16,7 +16,6 @@
"Folders": "Ordner",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Künstler",
- "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "weiter schauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Künstler",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index dc3a98154..f906d6e11 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -16,7 +16,6 @@
"Folders": "תיקיות",
"Genres": "ז'אנרים",
"HeaderAlbumArtists": "אמני האלבום",
- "HeaderCameraUploads": "העלאות ממצלמה",
"HeaderContinueWatching": "המשך לצפות",
"HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים",
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
new file mode 100644
index 000000000..df68d3bbd
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -0,0 +1,3 @@
+{
+ "Albums": "आल्बुम्"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 97c77017b..9be91b724 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -5,18 +5,17 @@
"Artists": "Izvođači",
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
"Books": "Knjige",
- "CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
+ "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}",
"Collections": "Kolekcije",
- "DeviceOfflineWithName": "{0} se odspojilo",
- "DeviceOnlineWithName": "{0} je spojeno",
- "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
+ "DeviceOfflineWithName": "{0} je prekinuo vezu",
+ "DeviceOnlineWithName": "{0} je povezan",
+ "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
"HeaderAlbumArtists": "Izvođači na albumu",
- "HeaderCameraUploads": "Uvoz sa kamere",
"HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači",
@@ -24,95 +23,97 @@
"HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
- "HeaderNextUp": "Sljedeće je",
+ "HeaderNextUp": "Slijedi",
"HeaderRecordingGroups": "Grupa snimka",
- "HomeVideos": "Kućni videi",
+ "HomeVideos": "Kućni video",
"Inherit": "Naslijedi",
"ItemAddedWithName": "{0} je dodano u biblioteku",
- "ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
+ "ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije",
- "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
- "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
- "MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
+ "MessageApplicationUpdated": "Jellyfin server je ažuriran",
+ "MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
+ "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
"MixedContent": "Miješani sadržaj",
"Movies": "Filmovi",
"Music": "Glazba",
"MusicVideos": "Glazbeni spotovi",
"NameInstallFailed": "{0} neuspješnih instalacija",
"NameSeasonNumber": "Sezona {0}",
- "NameSeasonUnknown": "Nepoznata sezona",
+ "NameSeasonUnknown": "Sezona nepoznata",
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
- "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
- "NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
- "NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
- "NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
- "NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
- "NotificationOptionInstallationFailed": "Instalacija neuspješna",
- "NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
- "NotificationOptionPluginError": "Dodatak otkazao",
+ "NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
+ "NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
+ "NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
+ "NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
+ "NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
+ "NotificationOptionInstallationFailed": "Instalacija nije uspjela",
+ "NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
+ "NotificationOptionPluginError": "Dodatak zakazao",
"NotificationOptionPluginInstalled": "Dodatak instaliran",
- "NotificationOptionPluginUninstalled": "Dodatak uklonjen",
- "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak",
- "NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera",
- "NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen",
+ "NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
+ "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
+ "NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
+ "NotificationOptionTaskFailed": "Greška zakazanog zadatka",
"NotificationOptionUserLockedOut": "Korisnik zaključan",
- "NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
- "NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
- "Photos": "Slike",
- "Playlists": "Popis za reprodukciju",
+ "NotificationOptionVideoPlayback": "Reprodukcija videa započela",
+ "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
+ "Photos": "Fotografije",
+ "Playlists": "Popisi za reprodukciju",
"Plugin": "Dodatak",
"PluginInstalledWithName": "{0} je instalirano",
"PluginUninstalledWithName": "{0} je deinstalirano",
"PluginUpdatedWithName": "{0} je ažurirano",
- "ProviderValue": "Pružitelj: {0}",
+ "ProviderValue": "Pružatelj: {0}",
"ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto",
- "ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
+ "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Serije",
"Songs": "Pjesme",
- "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
+ "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
- "SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
- "Sync": "Sink.",
- "System": "Sistem",
+ "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
+ "Sync": "Sinkronizacija",
+ "System": "Sustav",
"TvShows": "Serije",
"User": "Korisnik",
- "UserCreatedWithName": "Korisnik {0} je stvoren",
+ "UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan",
- "UserDownloadingItemWithValues": "{0} se preuzima {1}",
+ "UserDownloadingItemWithValues": "{0} preuzima {1}",
"UserLockedOutWithName": "Korisnik {0} je zaključan",
- "UserOfflineFromDevice": "{0} se odspojilo od {1}",
- "UserOnlineFromDevice": "{0} je online od {1}",
+ "UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
+ "UserOnlineFromDevice": "{0} povezan od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
- "UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
- "UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
- "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
+ "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
+ "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
+ "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
- "ValueSpecialEpisodeName": "Specijal - {0}",
+ "ValueSpecialEpisodeName": "Posebno - {0}",
"VersionNumber": "Verzija {0}",
- "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
- "TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
- "TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
- "TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
- "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
- "TaskCleanCache": "Očisti priručnu memoriju",
+ "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
+ "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
+ "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
+ "TaskRefreshChapterImages": "Izdvoji slike poglavlja",
+ "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
+ "TaskCleanCache": "Očisti mapu predmemorije",
"TasksApplicationCategory": "Aplikacija",
"TasksMaintenanceCategory": "Održavanje",
- "TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
- "TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
- "TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
+ "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
+ "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
+ "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
"TaskRefreshChannels": "Osvježi kanale",
- "TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
- "TaskCleanTranscode": "Očisti direktorij za transkodiranje",
- "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
+ "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
+ "TaskCleanTranscode": "Očisti mapu transkodiranja",
+ "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
"TaskUpdatePlugins": "Ažuriraj dodatke",
- "TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
- "TaskRefreshPeople": "Osvježi ljude",
- "TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
- "TaskCleanLogs": "Očisti direktorij sa logovima",
+ "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
+ "TaskRefreshPeople": "Osvježi osobe",
+ "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
+ "TaskCleanLogs": "Očisti mapu dnevnika zapisa",
"TasksChannelsCategory": "Internet kanali",
- "TasksLibraryCategory": "Biblioteka"
+ "TasksLibraryCategory": "Biblioteka",
+ "TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
+ "TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index c5c3844e3..343d213d4 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -16,7 +16,6 @@
"Folders": "Könyvtárak",
"Genres": "Műfajok",
"HeaderAlbumArtists": "Album előadók",
- "HeaderCameraUploads": "Kamera feltöltések",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 585fc6f02..ef3ed2580 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -20,7 +20,6 @@
"HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Lanjut Menonton",
- "HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis",
"Genres": "Aliran",
"Folders": "Folder",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 0f0f9130b..0f769eaad 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -13,7 +13,6 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa",
- "HeaderCameraUploads": "Myndavéla upphal",
"HeaderAlbumArtists": "Höfundur plötu",
"Genres": "Tegundir",
"Folders": "Möppur",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index bf1a0ef13..9e37ddc27 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -16,7 +16,6 @@
"Folders": "Cartelle",
"Genres": "Generi",
"HeaderAlbumArtists": "Artisti degli Album",
- "HeaderCameraUploads": "Caricamenti Fotocamera",
"HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti",
@@ -114,5 +113,7 @@
"TasksChannelsCategory": "Canali su Internet",
"TasksApplicationCategory": "Applicazione",
"TasksLibraryCategory": "Libreria",
- "TasksMaintenanceCategory": "Manutenzione"
+ "TasksMaintenanceCategory": "Manutenzione",
+ "TaskCleanActivityLog": "Attività di Registro Completate",
+ "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index a4d9f9ef6..02bf8496f 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -16,7 +16,6 @@
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
- "HeaderCameraUploads": "カメラアップロード",
"HeaderContinueWatching": "視聴を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
@@ -97,7 +96,7 @@
"TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
"TaskRefreshLibrary": "メディアライブラリのスキャン",
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
- "TaskCleanCache": "キャッシュの掃除",
+ "TaskCleanCache": "キャッシュを消去",
"TasksChannelsCategory": "ネットチャンネル",
"TasksApplicationCategory": "アプリケーション",
"TasksLibraryCategory": "ライブラリ",
@@ -113,5 +112,7 @@
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
"TaskRefreshChapterImages": "チャプター画像を抽出する",
- "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
+ "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
+ "TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
+ "TaskCleanActivityLog": "アクティビティの履歴を消去"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 5618ff4a8..91c1fb15b 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -16,7 +16,6 @@
"Folders": "Qaltalar",
"Genres": "Janrlar",
"HeaderAlbumArtists": "Álbom oryndaýshylary",
- "HeaderCameraUploads": "Kameradan júktelgender",
"HeaderContinueWatching": "Qaraýdy jalǵastyrý",
"HeaderFavoriteAlbums": "Tańdaýly álbomdar",
"HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index a33953c27..b8b39833c 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -16,7 +16,6 @@
"Folders": "폴더",
"Genres": "장르",
"HeaderAlbumArtists": "앨범 아티스트",
- "HeaderCameraUploads": "카메라 업로드",
"HeaderContinueWatching": "계속 시청하기",
"HeaderFavoriteAlbums": "즐겨찾는 앨범",
"HeaderFavoriteArtists": "즐겨찾는 아티스트",
@@ -28,7 +27,7 @@
"HeaderRecordingGroups": "녹화 그룹",
"HomeVideos": "홈 비디오",
"Inherit": "상속",
- "ItemAddedWithName": "{0}가 라이브러리에 추가됨",
+ "ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
"ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
"LabelIpAddressValue": "IP 주소: {0}",
"LabelRunningTimeValue": "상영 시간: {0}",
@@ -114,5 +113,7 @@
"TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
"TaskCleanCache": "캐시 폴더 청소",
"TasksChannelsCategory": "인터넷 채널",
- "TasksLibraryCategory": "라이브러리"
+ "TasksLibraryCategory": "라이브러리",
+ "TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
+ "TaskCleanActivityLog": "활동내역청소"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 35053766b..d4cb592ef 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -16,7 +16,6 @@
"Folders": "Katalogai",
"Genres": "Žanrai",
"HeaderAlbumArtists": "Albumo atlikėjai",
- "HeaderCameraUploads": "Kameros",
"HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteAlbums": "Mėgstami Albumai",
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index dbcf17287..5e3acfbe9 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -72,7 +72,6 @@
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV",
"HeaderContinueWatching": "Turpināt Skatīšanos",
- "HeaderCameraUploads": "Kameras augšupielādes",
"HeaderAlbumArtists": "Albumu Izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index bbdf99aba..b780ef498 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -51,7 +51,6 @@
"HeaderFavoriteArtists": "Омилени Изведувачи",
"HeaderFavoriteAlbums": "Омилени Албуми",
"HeaderContinueWatching": "Продолжи со гледање",
- "HeaderCameraUploads": "Поставувања од камера",
"HeaderAlbumArtists": "Изведувачи од Албуми",
"Genres": "Жанрови",
"Folders": "Папки",
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index b6db2b0f2..fdb4171b5 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -54,7 +54,6 @@
"ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
"HomeVideos": "घरचे व्हिडीयो",
"HeaderRecordingGroups": "रेकॉर्डिंग गट",
- "HeaderCameraUploads": "कॅमेरा अपलोड",
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
"Application": "अ‍ॅप्लिकेशन",
"AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 7f8df1289..5e3d095ff 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -16,7 +16,6 @@
"Folders": "Fail-fail",
"Genres": "Genre-genre",
"HeaderAlbumArtists": "Album Artis-artis",
- "HeaderCameraUploads": "Muatnaik Kamera",
"HeaderContinueWatching": "Terus Menonton",
"HeaderFavoriteAlbums": "Album-album Kegemaran",
"HeaderFavoriteArtists": "Artis-artis Kegemaran",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 07a599121..245c3cd63 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -16,7 +16,6 @@
"Folders": "Mapper",
"Genres": "Sjangre",
"HeaderAlbumArtists": "Albumartister",
- "HeaderCameraUploads": "Kameraopplastinger",
"HeaderContinueWatching": "Fortsett å se",
"HeaderFavoriteAlbums": "Favorittalbum",
"HeaderFavoriteArtists": "Favorittartister",
diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
index 38c073709..8e820d40c 100644
--- a/Emby.Server.Implementations/Localization/Core/ne.json
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -41,7 +41,6 @@
"HeaderFavoriteArtists": "मनपर्ने कलाकारहरू",
"HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू",
"HeaderContinueWatching": "हेर्न जारी राख्नुहोस्",
- "HeaderCameraUploads": "क्यामेरा अपलोडहरू",
"HeaderAlbumArtists": "एल्बमका कलाकारहरू",
"Genres": "विधाहरू",
"Folders": "फोल्डरहरू",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 41c74d54d..e102b92b9 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -16,7 +16,6 @@
"Folders": "Mappen",
"Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten",
- "HeaderCameraUploads": "Camera-uploads",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index fb6e81beb..6236515b2 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -19,7 +19,6 @@
"HeaderFavoriteArtists": "Favoritt Artistar",
"HeaderFavoriteAlbums": "Favoritt Album",
"HeaderContinueWatching": "Fortsett å sjå",
- "HeaderCameraUploads": "Kamera Opplastingar",
"HeaderAlbumArtists": "Album Artist",
"Genres": "Sjangrar",
"Folders": "Mapper",
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index bdc0d0169..003e591b3 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -16,7 +16,6 @@
"Folders": "Foldery",
"Genres": "Gatunki",
"HeaderAlbumArtists": "Wykonawcy albumów",
- "HeaderCameraUploads": "Przekazane obrazy",
"HeaderContinueWatching": "Kontynuuj odtwarzanie",
"HeaderFavoriteAlbums": "Ulubione albumy",
"HeaderFavoriteArtists": "Ulubieni wykonawcy",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 275195640..5e49ca702 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -16,7 +16,6 @@
"Folders": "Pastas",
"Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Envios da Câmera",
"HeaderContinueWatching": "Continuar Assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index c1fb65743..90a4941c5 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -16,7 +16,6 @@
"Folders": "Pastas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Envios a partir da câmara",
"HeaderContinueWatching": "Continuar a Ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
@@ -26,7 +25,7 @@
"HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação",
- "HomeVideos": "Videos caseiros",
+ "HomeVideos": "Vídeos Caseiros",
"Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index b534d0bbe..2079940cd 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -83,7 +83,6 @@
"Playlists": "Listas de Reprodução",
"Photos": "Fotografias",
"Movies": "Filmes",
- "HeaderCameraUploads": "Carregamentos a partir da câmara",
"FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
"DeviceOnlineWithName": "{0} está connectado",
"DeviceOfflineWithName": "{0} desconectou-se",
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 699dd26da..bc008df3b 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -74,7 +74,6 @@
"HeaderFavoriteArtists": "Artiști Favoriți",
"HeaderFavoriteAlbums": "Albume Favorite",
"HeaderContinueWatching": "Vizionează în continuare",
- "HeaderCameraUploads": "Incărcări Cameră Foto",
"HeaderAlbumArtists": "Album Artiști",
"Genres": "Genuri",
"Folders": "Dosare",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 648aa384b..c0db2cf7f 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -16,7 +16,6 @@
"Folders": "Папки",
"Genres": "Жанры",
"HeaderAlbumArtists": "Исполнители альбома",
- "HeaderCameraUploads": "Камеры",
"HeaderContinueWatching": "Продолжение просмотра",
"HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители",
@@ -114,5 +113,7 @@
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
- "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
+ "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.",
+ "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
+ "TaskCleanActivityLog": "Очистить журнал активности"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 0ee652637..8e5026944 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -16,7 +16,6 @@
"Folders": "Priečinky",
"Genres": "Žánre",
"HeaderAlbumArtists": "Umelci albumu",
- "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovať v pozeraní",
"HeaderFavoriteAlbums": "Obľúbené albumy",
"HeaderFavoriteArtists": "Obľúbení umelci",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 329c562e7..66681f025 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -3,21 +3,20 @@
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
"Application": "Aplikacija",
"Artists": "Izvajalci",
- "AuthenticationSucceededWithUserName": "{0} preverjanje pristnosti uspešno",
+ "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil",
"Books": "Knjige",
- "CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}",
+ "CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavje {0}",
"Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan",
- "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
+ "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
"Favorites": "Priljubljeno",
"Folders": "Mape",
"Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma",
- "HeaderCameraUploads": "Posnetki kamere",
- "HeaderContinueWatching": "Nadaljuj gledanje",
+ "HeaderContinueWatching": "Nadaljuj z ogledom",
"HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
"HeaderFavoriteEpisodes": "Priljubljene epizode",
@@ -33,23 +32,23 @@
"LabelIpAddressValue": "IP naslov: {0}",
"LabelRunningTimeValue": "Čas trajanja: {0}",
"Latest": "Najnovejše",
- "MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
- "MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
+ "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
+ "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitev {0} je bil posodobljen",
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
- "MixedContent": "Razne vsebine",
+ "MixedContent": "Mešane vsebine",
"Movies": "Filmi",
"Music": "Glasba",
"MusicVideos": "Glasbeni videi",
"NameInstallFailed": "{0} namestitev neuspešna",
"NameSeasonNumber": "Sezona {0}",
- "NameSeasonUnknown": "Season neznana",
+ "NameSeasonUnknown": "Neznana sezona",
"NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.",
"NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo",
"NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena",
- "NotificationOptionAudioPlayback": "Predvajanje zvoka začeto",
- "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno",
- "NotificationOptionCameraImageUploaded": "Posnetek kamere naložen",
+ "NotificationOptionAudioPlayback": "Predvajanje zvoka se je začelo",
+ "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka se je ustavilo",
+ "NotificationOptionCameraImageUploaded": "Fotografija naložena",
"NotificationOptionInstallationFailed": "Namestitev neuspešna",
"NotificationOptionNewLibraryContent": "Nove vsebine dodane",
"NotificationOptionPluginError": "Napaka dodatka",
@@ -57,41 +56,41 @@
"NotificationOptionPluginUninstalled": "Dodatek odstranjen",
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
- "NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
+ "NotificationOptionTaskFailed": "Načrtovano opravilo neuspešno",
"NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
"Photos": "Fotografije",
"Playlists": "Seznami predvajanja",
- "Plugin": "Plugin",
+ "Plugin": "Dodatek",
"PluginInstalledWithName": "{0} je bil nameščen",
"PluginUninstalledWithName": "{0} je bil odstranjen",
"PluginUpdatedWithName": "{0} je bil posodobljen",
- "ProviderValue": "Provider: {0}",
+ "ProviderValue": "Ponudnik: {0}",
"ScheduledTaskFailedWithName": "{0} ni uspelo",
"ScheduledTaskStartedWithName": "{0} začeto",
"ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
"Shows": "Serije",
"Songs": "Pesmi",
- "StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
+ "StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
"Sync": "Sinhroniziraj",
- "System": "System",
+ "System": "Sistem",
"TvShows": "TV serije",
- "User": "User",
+ "User": "Uporabnik",
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
"UserDownloadingItemWithValues": "{0} prenaša {1}",
"UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
- "UserOnlineFromDevice": "{0} je aktiven iz {1}",
+ "UserOnlineFromDevice": "{0} je aktiven na {1}",
"UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
"UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
- "ValueSpecialEpisodeName": "Poseben - {0}",
+ "ValueSpecialEpisodeName": "Posebna - {0}",
"VersionNumber": "Različica {0}",
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
@@ -103,7 +102,7 @@
"TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.",
"TaskRefreshPeople": "Osveži osebe",
"TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.",
- "TaskCleanLogs": "Počisti mapo dnevnika",
+ "TaskCleanLogs": "Počisti mapo dnevnikov",
"TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.",
"TaskRefreshLibrary": "Preišči knjižnico predstavnosti",
"TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.",
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index 347ba5f97..0d909b06e 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -96,7 +96,6 @@
"HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
- "HeaderCameraUploads": "Ngarkimet nga Kamera",
"HeaderAlbumArtists": "Artistët e albumeve",
"Genres": "Zhanre",
"Folders": "Dosje",
@@ -113,5 +112,5 @@
"Artists": "Artistë",
"Application": "Aplikacioni",
"AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
- "Albums": "Albumet"
+ "Albums": "Albume"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 5f3cbb1c8..2b1eccfaf 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -74,7 +74,6 @@
"HeaderFavoriteArtists": "Омиљени извођачи",
"HeaderFavoriteAlbums": "Омиљени албуми",
"HeaderContinueWatching": "Настави гледање",
- "HeaderCameraUploads": "Слања са камере",
"HeaderAlbumArtists": "Извођачи албума",
"Genres": "Жанрови",
"Folders": "Фасцикле",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index c8662b2ca..bea294ba2 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -9,14 +9,13 @@
"Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar",
- "DeviceOfflineWithName": "{0} har kopplat från",
+ "DeviceOfflineWithName": "{0} har kopplat ner",
"DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter",
"Folders": "Mappar",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumartister",
- "HeaderCameraUploads": "Kamerauppladdningar",
"HeaderContinueWatching": "Fortsätt kolla",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 810b1b9ab..8089fc304 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -20,7 +20,6 @@
"MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
"Inherit": "மரபுரிமையாகப் பெறு",
"HeaderRecordingGroups": "பதிவு குழுக்கள்",
- "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
"Folders": "கோப்புறைகள்",
"FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
@@ -102,7 +101,7 @@
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
- "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
+ "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 3b77215a3..71dd2c7a3 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -50,7 +50,6 @@
"HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
"HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ",
- "HeaderCameraUploads": "อัปโหลดรูปถ่าย",
"HeaderAlbumArtists": "อัลบั้มศิลปิน",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 3cf3482eb..54d3a65f0 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar",
"ChapterNameValue": "Bölüm {0}",
- "Collections": "Koleksiyonlar",
+ "Collections": "Koleksiyon",
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
@@ -16,7 +16,6 @@
"Folders": "Klasörler",
"Genres": "Türler",
"HeaderAlbumArtists": "Albüm Sanatçıları",
- "HeaderCameraUploads": "Kamera Yüklemeleri",
"HeaderContinueWatching": "İzlemeye Devam Et",
"HeaderFavoriteAlbums": "Favori Albümler",
"HeaderFavoriteArtists": "Favori Sanatçılar",
@@ -24,7 +23,7 @@
"HeaderFavoriteShows": "Favori Diziler",
"HeaderFavoriteSongs": "Favori Şarkılar",
"HeaderLiveTV": "Canlı TV",
- "HeaderNextUp": "Sonraki hafta",
+ "HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları",
"HomeVideos": "Ev videoları",
"Inherit": "Devral",
@@ -114,5 +113,7 @@
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
- "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler."
+ "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
+ "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
+ "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi."
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index e673465a4..06cc5f633 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -16,7 +16,6 @@
"HeaderFavoriteArtists": "Улюблені виконавці",
"HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд",
- "HeaderCameraUploads": "Завантажено з камери",
"HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри",
"Folders": "Каталоги",
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 9a5874e29..fa7b2d4d0 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -105,7 +105,6 @@
"Inherit": "وراثت میں",
"HomeVideos": "ہوم ویڈیو",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
- "HeaderCameraUploads": "کیمرہ اپلوڈز",
"FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 2392c8347..ac74deff8 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -103,7 +103,6 @@
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
"HeaderFavoriteAlbums": "Album Ưa Thích",
- "HeaderCameraUploads": "Máy Ảnh Tải Lên",
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
"DeviceOnlineWithName": "{0} đã kết nối",
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 6b563a9b1..3ae0fe5e7 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -16,7 +16,6 @@
"Folders": "文件夹",
"Genres": "风格",
"HeaderAlbumArtists": "专辑作家",
- "HeaderCameraUploads": "相机上传",
"HeaderContinueWatching": "继续观影",
"HeaderFavoriteAlbums": "收藏的专辑",
"HeaderFavoriteArtists": "最爱的艺术家",
@@ -114,5 +113,7 @@
"TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
"TaskCleanCache": "清理缓存目录",
"TasksApplicationCategory": "应用程序",
- "TasksMaintenanceCategory": "维护"
+ "TasksMaintenanceCategory": "维护",
+ "TaskCleanActivityLog": "清理程序日志",
+ "TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 1ac62baca..435e294ef 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -16,7 +16,6 @@
"Folders": "檔案夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯藝人",
- "HeaderCameraUploads": "相機上載",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛的藝人",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 7b6540c3e..d2e3d77a3 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -16,7 +16,6 @@
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
- "HeaderCameraUploads": "相機上傳",
"HeaderContinueWatching": "繼續觀賞",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛演出者",
@@ -113,5 +112,7 @@
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
"TasksChannelsCategory": "網路頻道",
"TasksApplicationCategory": "應用程式",
- "TasksMaintenanceCategory": "維修"
+ "TasksMaintenanceCategory": "維護",
+ "TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。",
+ "TaskCleanActivityLog": "清除活動紀錄"
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index bc01f9543..56f4133a0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -703,7 +703,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
MaxRuntimeTicks = info.MaxRuntimeTicks
};
- if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -717,7 +717,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
};
}
- if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -737,7 +737,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
};
}
- if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
{
if (!info.IntervalTicks.HasValue)
{
@@ -751,7 +751,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
};
}
- if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
{
return new StartupTrigger();
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
new file mode 100644
index 000000000..4abbf784b
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old activity log entries.
+ /// </summary>
+ public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ private readonly ILocalizationManager _localization;
+ private readonly IActivityManager _activityManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class.
+ /// </summary>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public CleanActivityLogTask(
+ ILocalizationManager localization,
+ IActivityManager activityManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _localization = localization;
+ _activityManager = activityManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanActivityLog");
+
+ /// <inheritdoc />
+ public string Key => "CleanActivityLog";
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public bool IsHidden => false;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <inheritdoc />
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
+ if (!retentionDays.HasValue || retentionDays <= 0)
+ {
+ throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
+ }
+
+ var startDate = DateTime.UtcNow.AddDays(retentionDays.Value * -1);
+ return _activityManager.CleanAsync(startDate);
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return Enumerable.Empty<TaskTriggerInfo>();
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 5adcefc1f..692d1667d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index 54e18eaea..184d155d4 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
return new[]
{
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index 691408167..26ef19354 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -148,7 +148,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public bool IsHidden => false;
/// <inheritdoc />
- public bool IsEnabled => false;
+ public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 29393ae07..4bc12f44a 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Security
{
if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var existingColumnNames = GetColumnNames(db, "AccessTokens");
@@ -88,7 +89,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
@@ -119,7 +121,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
@@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
@@ -346,7 +350,8 @@ namespace Emby.Server.Implementations.Security
{
using (var connection = GetConnection(true))
{
- return connection.RunInTransaction(db =>
+ return connection.RunInTransaction(
+ db =>
{
using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
{
@@ -377,7 +382,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
{
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index dfdd4200e..ac589b03c 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -104,6 +104,6 @@ namespace Emby.Server.Implementations
public string InternalMetadataPath { get; set; }
/// <inheritdoc />
- public string VirtualInternalMetadataPath { get; } = "%MetadataPath%";
+ public string VirtualInternalMetadataPath => "%MetadataPath%";
}
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index e42d47853..607b322f2 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -666,7 +666,7 @@ namespace Emby.Server.Implementations.Session
}
}
- var eventArgs = new PlaybackProgressEventArgs
+ var eventArgs = new PlaybackStartEventArgs
{
Item = libraryItem,
Users = users,
@@ -1064,10 +1064,10 @@ namespace Emby.Server.Implementations.Session
AssertCanControl(session, controllingSession);
}
- return SendMessageToSession(session, "GeneralCommand", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
}
- private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
+ private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken)
{
var controllers = session.SessionControllers;
var messageId = Guid.NewGuid();
@@ -1078,7 +1078,7 @@ namespace Emby.Server.Implementations.Session
}
}
- private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, string name, T data, CancellationToken cancellationToken)
+ private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
{
IEnumerable<Task> GetTasks()
{
@@ -1178,7 +1178,7 @@ namespace Emby.Server.Implementations.Session
}
}
- await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -1186,7 +1186,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var session = GetSessionToRemoteControl(sessionId);
- await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -1194,7 +1194,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var session = GetSessionToRemoteControl(sessionId);
- await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
}
}
- return SendMessageToSession(session, "Playstate", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken);
}
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
@@ -1322,7 +1322,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
}
/// <summary>
@@ -1334,7 +1334,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
}
/// <summary>
@@ -1348,7 +1348,7 @@ namespace Emby.Server.Implementations.Session
_logger.LogDebug("Beginning SendServerRestartNotification");
- return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
}
/// <summary>
@@ -1484,6 +1484,14 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("User is not allowed access from this device.");
}
+ int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
+ int maxActiveSessions = user.MaxActiveSessions;
+ _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions);
+ if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
+ {
+ throw new SecurityException("User is at their maximum number of sessions.");
+ }
+
var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
var session = LogSessionActivity(
@@ -1866,7 +1874,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1879,7 +1887,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1894,7 +1902,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1903,7 +1911,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 15c2af220..a5f847953 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -8,6 +8,7 @@ using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -316,7 +317,7 @@ namespace Emby.Server.Implementations.Session
return webSocket.SendAsync(
new WebSocketMessage<int>
{
- MessageType = "ForceKeepAlive",
+ MessageType = SessionMessageType.ForceKeepAlive,
Data = WebSocketLostTimeout
},
CancellationToken.None);
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 94604ca1e..b986ffa1c 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
@@ -65,7 +66,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public Task SendMessage<T>(
- string name,
+ SessionMessageType name,
Guid messageId,
T data,
CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
index 80b977731..538479512 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
@@ -301,8 +301,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (_group.IsPaused)
{
// Pick a suitable time that accounts for latency
- var delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
+ var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
// Unpause group and set starting point in future
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
@@ -452,8 +451,7 @@ namespace Emby.Server.Implementations.SyncPlay
else
{
// Client, that was buffering, resumed playback but did not update others in time
- delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
+ delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
_group.LastActivity = currentTime.AddMilliseconds(
delay);
@@ -497,7 +495,7 @@ namespace Emby.Server.Implementations.SyncPlay
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
{
// Collected pings are used to account for network latency when unpausing playback
- _group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
+ _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index d1818deff..ccd1446dd 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.TV
.GetItemList(
new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
SeriesPresentationUniqueKey = presentationUniqueKey,
Limit = limit,
@@ -214,7 +214,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.Ascending) },
Limit = 1,
IsPlayed = false,
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 003cf3c74..fd1f43e62 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -10,19 +10,21 @@ using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
-using MediaBrowser.Common.System;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.System;
namespace Emby.Server.Implementations.Updates
{
@@ -36,6 +38,7 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ILogger<InstallationManager> _logger;
private readonly IApplicationPaths _appPaths;
+ private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerConfigurationManager _config;
@@ -45,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
/// Gets the application host.
/// </summary>
/// <value>The application host.</value>
- private readonly IApplicationHost _applicationHost;
+ private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient;
@@ -63,25 +66,22 @@ namespace Emby.Server.Implementations.Updates
public InstallationManager(
ILogger<InstallationManager> logger,
- IApplicationHost appHost,
+ IServerApplicationHost appHost,
IApplicationPaths appPaths,
+ IEventManager eventManager,
IHttpClientFactory httpClientFactory,
IJsonSerializer jsonSerializer,
IServerConfigurationManager config,
IFileSystem fileSystem,
IZipClient zipClient)
{
- if (logger == null)
- {
- throw new ArgumentNullException(nameof(logger));
- }
-
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
_logger = logger;
_applicationHost = appHost;
_appPaths = appPaths;
+ _eventManager = eventManager;
_httpClientFactory = httpClientFactory;
_jsonSerializer = jsonSerializer;
_config = config;
@@ -90,27 +90,6 @@ namespace Emby.Server.Implementations.Updates
}
/// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstalling;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstallationCompleted;
-
- /// <inheritdoc />
- public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstallationCancelled;
-
- /// <inheritdoc />
- public event EventHandler<IPlugin> PluginUninstalled;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PluginUpdated;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PluginInstalled;
-
- /// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
@@ -237,7 +216,8 @@ namespace Emby.Server.Implementations.Updates
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
- foreach (var plugin in _applicationHost.Plugins)
+ var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
+ foreach (var plugin in plugins)
{
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
@@ -268,11 +248,11 @@ namespace Emby.Server.Implementations.Updates
var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
- PackageInstalling?.Invoke(this, package);
+ await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
try
{
- await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
+ var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
lock (_currentInstallationsLock)
{
@@ -280,8 +260,11 @@ namespace Emby.Server.Implementations.Updates
}
_completedInstallationsInternal.Add(package);
+ await _eventManager.PublishAsync(isUpdate
+ ? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
+ : new PluginInstalledEventArgs(package)).ConfigureAwait(false);
- PackageInstallationCompleted?.Invoke(this, package);
+ _applicationHost.NotifyPendingRestart();
}
catch (OperationCanceledException)
{
@@ -292,7 +275,7 @@ namespace Emby.Server.Implementations.Updates
_logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
- PackageInstallationCancelled?.Invoke(this, package);
+ await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false);
throw;
}
@@ -305,11 +288,11 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Remove(tuple);
}
- PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs
+ await _eventManager.PublishAsync(new InstallationFailedEventArgs
{
InstallationInfo = package,
Exception = ex
- });
+ }).ConfigureAwait(false);
throw;
}
@@ -326,7 +309,7 @@ namespace Emby.Server.Implementations.Updates
/// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
- private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
+ private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
@@ -336,20 +319,9 @@ namespace Emby.Server.Implementations.Updates
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing
- if (plugin == null)
- {
- _logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version);
+ _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
- PluginInstalled?.Invoke(this, package);
- }
- else
- {
- _logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version);
-
- PluginUpdated?.Invoke(this, package);
- }
-
- _applicationHost.NotifyPendingRestart();
+ return plugin != null;
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@@ -467,7 +439,7 @@ namespace Emby.Server.Implementations.Updates
_config.SaveConfiguration();
}
- PluginUninstalled?.Invoke(this, plugin);
+ _eventManager.Publish(new PluginUninstalledEventArgs(plugin));
_applicationHost.NotifyPendingRestart();
}
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index d732b6bc6..7d68aecf9 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -50,6 +50,13 @@ namespace Jellyfin.Api.Auth
bool localAccessOnly = false,
bool requiredDownloadPermission = false)
{
+ // ApiKey is currently global admin, always allow.
+ var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
+ if (isApiKey)
+ {
+ return true;
+ }
+
// Ensure claim has userId.
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
if (!userId.HasValue)
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 733c6959e..27a1f61be 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -1,10 +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.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
@@ -43,24 +43,23 @@ namespace Jellyfin.Api.Auth
try
{
var authorizationInfo = _authService.Authenticate(Request);
- if (authorizationInfo == null)
+ var role = UserRoles.User;
+ if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
{
- return Task.FromResult(AuthenticateResult.NoResult());
- // TODO return when legacy API is removed.
- // Don't spam the log with "Invalid User"
- // return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
+ role = UserRoles.Administrator;
}
var claims = new[]
{
- new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
- new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
+ new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
+ new Claim(ClaimTypes.Role, role),
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),
+ new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
index b5913daab..be77b7a4e 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{
var validated = ValidateClaims(context.User);
- if (!validated)
+ if (validated)
+ {
+ context.Succeed(requirement);
+ }
+ else
{
context.Fail();
- return Task.CompletedTask;
}
- context.Succeed(requirement);
return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
index 5213bc4cb..a7623556a 100644
--- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
@@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
{
var validated = ValidateClaims(context.User, ignoreSchedule: true);
- if (!validated)
+ if (validated)
+ {
+ context.Succeed(requirement);
+ }
+ else
{
context.Fail();
- return Task.CompletedTask;
}
- context.Succeed(requirement);
return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
index af73352bc..d772ec554 100644
--- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
@@ -29,13 +29,13 @@ namespace Jellyfin.Api.Auth.LocalAccessPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
{
var validated = ValidateClaims(context.User, localAccessOnly: true);
- if (!validated)
+ if (validated)
{
- context.Fail();
+ context.Succeed(requirement);
}
else
{
- context.Succeed(requirement);
+ context.Fail();
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs
index 4d7c7135d..8323312e5 100644
--- a/Jellyfin.Api/Constants/InternalClaimTypes.cs
+++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs
@@ -34,5 +34,10 @@
/// Token.
/// </summary>
public const string Token = "Jellyfin-Token";
+
+ /// <summary>
+ /// Is Api Key.
+ /// </summary>
+ public const string IsApiKey = "Jellyfin-IsApiKey";
}
}
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index a07cea9c0..b429cebec 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,7 +1,7 @@
using System;
-using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -39,19 +39,19 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
[HttpGet("Entries")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
+ public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] DateTime? minDate,
[FromQuery] bool? hasUserId)
{
- var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
- entries => entries.Where(entry => entry.DateCreated >= minDate
- && (!hasUserId.HasValue || (hasUserId.Value
- ? entry.UserId != Guid.Empty
- : entry.UserId == Guid.Empty))));
-
- return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
+ return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
+ {
+ StartIndex = startIndex,
+ Limit = limit,
+ MinDate = minDate,
+ HasUserId = hasUserId
+ }).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index d38214116..826fce6b0 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -4,11 +4,13 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
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;
@@ -54,7 +56,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
@@ -89,7 +91,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -99,7 +101,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -145,9 +147,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
@@ -188,7 +190,7 @@ namespace Jellyfin.Api.Controllers
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -263,7 +265,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
@@ -298,7 +300,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -308,7 +310,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -354,9 +356,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
@@ -397,7 +399,7 @@ namespace Jellyfin.Api.Controllers
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 33a969f85..20fc96ba8 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -105,7 +106,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <response code="200">Channel items returned.</response>
@@ -121,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? sortOrder,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? sortBy,
[FromQuery] string? fields)
{
@@ -140,7 +141,7 @@ namespace Jellyfin.Api.Controllers
.AddItemFields(fields)
};
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -183,7 +184,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">Optional. User Id.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
/// <response code="200">Latest channel items returned.</response>
@@ -196,7 +197,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? fields,
[FromQuery] string? channelIds)
{
@@ -217,7 +218,7 @@ namespace Jellyfin.Api.Controllers
.AddItemFields(fields)
};
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 2fc697a6a..eae06b767 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -83,14 +83,14 @@ namespace Jellyfin.Api.Controllers
/// Adds items to a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
- /// <param name="itemIds">Item ids, comma delimited.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
/// <response code="204">Items added to collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
+ public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{
- await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true);
+ await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
return NoContent();
}
@@ -98,14 +98,14 @@ namespace Jellyfin.Api.Controllers
/// Removes items from a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
- /// <param name="itemIds">Item ids, comma delimited.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
/// <response code="204">Items removed from collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
+ public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{
- await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false);
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 74380c2ef..b3e3490c2 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Devices Controller.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.RequiresElevation)]
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
@@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Devices retrieved.</response>
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{
@@ -62,7 +61,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response>
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpGet("Info")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
@@ -84,7 +82,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response>
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpGet("Options")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
@@ -107,7 +104,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpPost("Options")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 874467c75..76f5717e3 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -81,6 +81,9 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+ // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
+ _displayPreferencesManager.SaveChanges();
+
return dto;
}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 271ae293b..4e6455eaa 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -77,6 +77,7 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
@@ -94,6 +95,7 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
@@ -111,8 +113,12 @@ namespace Jellyfin.Api.Controllers
/// Process a content directory control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
@@ -122,8 +128,12 @@ namespace Jellyfin.Api.Controllers
/// Process a connection manager control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
@@ -133,8 +143,12 @@ namespace Jellyfin.Api.Controllers
/// Process a media receiver registrar control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
@@ -144,11 +158,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{
return ProcessEventRequest(_mediaReceiverRegistrar);
@@ -158,11 +176,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{
return ProcessEventRequest(_contentDirectory);
@@ -172,11 +194,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{
return ProcessEventRequest(_connectionManager);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 1153a601e..e07690e11 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -295,6 +295,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@@ -351,6 +352,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -403,7 +405,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@@ -623,6 +625,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@@ -677,6 +680,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -729,7 +733,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@@ -959,6 +963,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@@ -1017,6 +1022,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -1069,7 +1075,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index de6aa86c9..4e47658b0 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -10,6 +9,7 @@ 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;
@@ -47,7 +47,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all genres from a given item, folder, or the entire library.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
@@ -55,22 +54,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@@ -82,7 +68,6 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -90,22 +75,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
- [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -116,42 +88,22 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -169,87 +121,20 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
+ QueryResult<(BaseItem, ItemCounts)> result;
+ if (parentItem is ICollectionFolder parentCollectionFolder
+ && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
+ || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
{
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
+ result = _libraryManager.GetMusicGenres(query);
}
-
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ else
{
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
+ result = _libraryManager.GetGenres(query);
}
- var result = new QueryResult<(BaseItem, ItemCounts)>();
-
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 7afec1219..4a67c1aed 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -109,7 +109,7 @@ namespace Jellyfin.Api.Controllers
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
- _userManager.ClearProfileImage(user);
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
@@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult DeleteUserImage(
+ public async Task<ActionResult> DeleteUserImage(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null)
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
_logger.LogError(e, "Error deleting user profile image:");
}
- _userManager.ClearProfileImage(user);
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
}
@@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@@ -364,7 +364,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? quality,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
- [FromQuery] string? format,
+ [FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
@@ -443,7 +443,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? quality,
[FromRoute, Required] string tag,
[FromQuery] bool? cropWhitespace,
- [FromRoute, Required] string format,
+ [FromRoute, Required] ImageFormat format,
[FromQuery] bool? addPlayedIndicator,
[FromRoute, Required] double percentPlayed,
[FromRoute, Required] int unplayedCount,
@@ -516,7 +516,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -595,7 +595,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -674,7 +674,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -753,7 +753,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -832,7 +832,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] string tag,
- [FromRoute, Required] string format,
+ [FromRoute, Required] ImageFormat format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -911,7 +911,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] string? tag,
- [FromQuery] string? format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -1038,7 +1038,7 @@ namespace Jellyfin.Api.Controllers
ImageType imageType,
int? imageIndex,
string? tag,
- string? format,
+ ImageFormat? format,
int? maxWidth,
int? maxHeight,
double? percentPlayed,
@@ -1128,12 +1128,11 @@ namespace Jellyfin.Api.Controllers
isHeadRequest).ConfigureAwait(false);
}
- private ImageFormat[] GetOutputFormats(string? format)
+ private ImageFormat[] GetOutputFormats(ImageFormat? format)
{
- if (!string.IsNullOrWhiteSpace(format)
- && Enum.TryParse(format, true, out ImageFormat parsedFormat))
+ if (format.HasValue)
{
- return new[] { parsedFormat };
+ return new[] { format.Value };
}
return GetClientSupportedFormats();
@@ -1157,7 +1156,7 @@ namespace Jellyfin.Api.Controllers
var acceptParam = Request.Query[HeaderNames.Accept];
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
if (!supportsWebP)
{
@@ -1179,7 +1178,7 @@ namespace Jellyfin.Api.Controllers
formats.Add(ImageFormat.Jpg);
formats.Add(ImageFormat.Png);
- if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
+ if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
{
formats.Add(ImageFormat.Gif);
}
@@ -1187,9 +1186,10 @@ namespace Jellyfin.Api.Controllers
return formats.ToArray();
}
- private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll)
{
- var mimeType = "image/" + format;
+ var normalized = format.ToString().ToLowerInvariant();
+ var mimeType = "image/" + normalized;
if (requestAcceptTypes.Contains(mimeType))
{
@@ -1201,7 +1201,7 @@ namespace Jellyfin.Api.Controllers
return true;
}
- return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
}
private async Task<ActionResult> GetImageResult(
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 07fed9764..7f8a2be12 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -71,7 +72,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -108,7 +109,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var album = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -145,7 +146,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -182,7 +183,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -218,7 +219,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -255,7 +256,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -292,7 +293,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -315,9 +316,9 @@ namespace Jellyfin.Api.Controllers
TotalRecordCount = list.Count
};
- if (limit.HasValue)
+ if (limit.HasValue && limit < list.Count)
{
- list = list.Take(limit.Value).ToList();
+ list = list.GetRange(0, limit.Value);
}
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 652c4689d..7ca577543 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] string? excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -182,10 +183,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
- [FromQuery] string? imageTypes,
+ [FromQuery] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
[FromQuery] string? genres,
@@ -194,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -342,7 +343,7 @@ namespace Jellyfin.Api.Controllers
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
- ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(),
+ ImageTypes = imageTypes,
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
AdjacentTo = adjacentTo,
ItemIds = RequestHelpers.GetGuids(ids),
@@ -365,7 +366,7 @@ namespace Jellyfin.Api.Controllers
query.CollapseBoxSetItems = false;
}
- foreach (var filter in RequestHelpers.GetFilters(filters!))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -406,12 +407,9 @@ namespace Jellyfin.Api.Controllers
}
// ExcludeLocationTypes
- if (!string.IsNullOrEmpty(excludeLocationTypes))
+ if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{
- if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual))
- {
- query.IsVirtualItem = false;
- }
+ query.IsVirtualItem = false;
}
if (!string.IsNullOrEmpty(locationTypes))
@@ -539,7 +537,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index d290e3c5b..94995650c 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -75,7 +76,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string? name,
[FromQuery] string? collectionType,
- [FromQuery] string[] paths,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false)
{
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 3557e6304..88a7542ce 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -26,6 +26,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
@@ -145,7 +146,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isDisliked,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy,
@@ -262,7 +263,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool? isMovie,
@@ -349,7 +350,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
@@ -560,7 +561,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? genreIds,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? seriesTimerId,
[FromQuery] Guid? librarySeriesId,
@@ -591,7 +592,7 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids,
IsSports = isSports,
SeriesTimerId = seriesTimerId,
- Genres = RequestHelpers.Split(genres, ',', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds)
};
@@ -647,7 +648,7 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids,
IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId,
- Genres = RequestHelpers.Split(body.Genres, ',', true),
+ Genres = RequestHelpers.Split(body.Genres, '|', true),
GenreIds = RequestHelpers.GetGuids(body.GenreIds)
};
@@ -704,7 +705,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSports,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? genreIds,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
@@ -1219,11 +1220,8 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- await using var memoryStream = new MemoryStream();
- await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
- .WriteToAsync(memoryStream, CancellationToken.None)
- .ConfigureAwait(false);
- return File(memoryStream, MimeTypes.GetMimeType("file." + container));
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
private void AssertUserCanManageLiveTv()
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 4c21999b1..186024585 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
@@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? openToken,
[FromQuery] Guid? userId,
[FromQuery] string? playSessionId,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 7fcfc749d..afb536e74 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers
IncludeItemTypes = new[]
{
nameof(Movie),
- // typeof(Trailer).Name,
- // typeof(LiveTvProgram).Name
+ // nameof(Trailer),
+ // nameof(LiveTvProgram)
},
// IsMovie = true
OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 570ae8fdc..e105befe8 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -11,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -47,7 +47,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all music genres from a given item, folder, or the entire library.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
@@ -55,22 +54,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@@ -80,8 +66,8 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Music genres returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
[HttpGet]
+ [Obsolete("Use GetGenres instead")]
public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -89,22 +75,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
- [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -115,42 +88,22 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -168,87 +121,10 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in RequestHelpers.GetFilters(filters))
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
var result = _libraryManager.GetMusicGenres(query);
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 8bd610dad..1e0bdb6bc 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,15 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using 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;
@@ -26,6 +27,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
/// <summary>
/// Initializes a new instance of the <see cref="PersonsController"/> class.
@@ -33,84 +35,53 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
public PersonsController(
ILibraryManager libraryManager,
IDtoService dtoService,
- IUserManager userManager)
+ IUserManager userManager,
+ IUserDataManager userDataManager)
{
_libraryManager = libraryManager;
_dtoService = dtoService;
_userManager = userManager;
+ _userDataManager = userDataManager;
}
/// <summary>
- /// Gets all persons from a given item, folder, or the entire library.
+ /// Gets all persons.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
/// <param name="userId">User id.</param>
- /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
- /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
- /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
- /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
/// <response code="200">Persons returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
- [FromQuery] double? minCommunityRating,
- [FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
[FromQuery] string? fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
+ [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery] string? excludePersonTypes,
[FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] string? appearsInItemId,
[FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
+ [FromQuery] bool? enableImages = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
@@ -118,136 +89,28 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
- BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
- var query = new InternalItemsQuery(user)
+ var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
+ var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
- };
-
- if (!string.IsNullOrWhiteSpace(parentId))
- {
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { new Guid(parentId) };
- }
- else
- {
- query.ItemIds = new[] { new Guid(parentId) };
- }
- }
-
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in RequestHelpers.GetFilters(filters))
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- var result = new QueryResult<(BaseItem, ItemCounts)>();
-
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
+ ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
+ NameContains = searchTerm,
+ User = user,
+ IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
+ AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
+ Limit = limit ?? 0
});
return new QueryResult<BaseItemDto>
{
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
+ Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(),
+ TotalRecordCount = peopleItems.Count
};
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 1e95bd2b3..0419b2436 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -151,7 +152,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null)
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index ab7920895..68e4f0586 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>The list of scheduled tasks.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<IScheduledTaskWorker> GetTasks(
+ public IEnumerable<TaskInfo> GetTasks(
[FromQuery] bool? isHidden,
[FromQuery] bool? isEnabled)
{
@@ -57,7 +57,7 @@ namespace Jellyfin.Api.Controllers
}
}
- yield return task;
+ yield return ScheduledTaskHelpers.GetTaskInfo(task);
}
}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 39bf6e6dc..e506ac7bf 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult PostCapabilities(
[FromQuery] string? id,
[FromQuery] string? playableMediaTypes,
- [FromQuery] string? supportedCommands,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
[FromQuery] bool supportsPersistentIdentifier = true)
@@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities
{
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
- SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true),
+ SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync,
SupportsPersistentIdentifier = supportsPersistentIdentifier
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index cdd5f958e..c5fcfb356 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -9,6 +8,7 @@ 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;
@@ -45,7 +45,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all studios from a given item, folder, or the entire library.
/// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param>
@@ -53,22 +52,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@@ -80,7 +67,6 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetStudios(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -88,22 +74,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -116,44 +90,23 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
- var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
- MediaTypes = mediaTypesArr,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -171,84 +124,9 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|').Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null).Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in RequestHelpers.GetFilters(filters))
- {
- switch (filter)
- {
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- }
- }
-
- var result = new QueryResult<(BaseItem, ItemCounts)>();
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, itemCounts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = itemCounts.ItemCount;
- dto.ProgramCount = itemCounts.ProgramCount;
- dto.SeriesCount = itemCounts.SeriesCount;
- dto.EpisodeCount = itemCounts.EpisodeCount;
- dto.MovieCount = itemCounts.MovieCount;
- dto.TrailerCount = itemCounts.TrailerCount;
- dto.AlbumCount = itemCounts.AlbumCount;
- dto.SongCount = itemCounts.SongCount;
- dto.ArtistCount = itemCounts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var result = _libraryManager.GetStudios(query);
+ var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index cc682ed54..a01ae31a0 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -11,6 +11,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.SubtitleDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -21,6 +24,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Subtitles;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -34,6 +38,7 @@ namespace Jellyfin.Api.Controllers
[Route("")]
public class SubtitleController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager;
private readonly ISubtitleEncoder _subtitleEncoder;
@@ -46,6 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
/// </summary>
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
@@ -55,6 +61,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
public SubtitleController(
+ IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager,
ISubtitleManager subtitleManager,
ISubtitleEncoder subtitleEncoder,
@@ -64,6 +71,7 @@ namespace Jellyfin.Api.Controllers
IAuthorizationContext authContext,
ILogger<SubtitleController> logger)
{
+ _serverConfigurationManager = serverConfigurationManager;
_libraryManager = libraryManager;
_subtitleManager = subtitleManager;
_subtitleEncoder = subtitleEncoder;
@@ -320,6 +328,33 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
+ /// Upload an external subtitle file.
+ /// </summary>
+ /// <param name="itemId">The item the subtitle belongs to.</param>
+ /// <param name="body">The request body.</param>
+ /// <response code="204">Subtitle uploaded.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Videos/{itemId}/Subtitles")]
+ public async Task<ActionResult> UploadSubtitle(
+ [FromRoute, Required] Guid itemId,
+ [FromBody, Required] UploadSubtitleDto body)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
+ var data = Convert.FromBase64String(body.Data);
+ await using var memoryStream = new MemoryStream(data);
+ await _subtitleManager.UploadSubtitle(
+ video,
+ new SubtitleResponse
+ {
+ Format = body.Format,
+ Language = body.Language,
+ IsForced = body.IsForced,
+ Stream = memoryStream
+ }).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
/// Encodes a subtitle in the specified format.
/// </summary>
/// <param name="id">The media id.</param>
@@ -351,5 +386,95 @@ namespace Jellyfin.Api.Controllers
copyTimestamps,
CancellationToken.None);
}
+
+ /// <summary>
+ /// Gets a list of available fallback font files.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
+ [HttpGet("FallbackFont/Fonts")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<FontFile> GetFallbackFontList()
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var fallbackFontPath = encodingOptions.FallbackFontPath;
+
+ if (!string.IsNullOrEmpty(fallbackFontPath))
+ {
+ var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
+ var fontFiles = files
+ .Select(i => new FontFile
+ {
+ Name = i.Name,
+ Size = i.Length,
+ DateCreated = _fileSystem.GetCreationTimeUtc(i),
+ DateModified = _fileSystem.GetLastWriteTimeUtc(i)
+ })
+ .OrderBy(i => i.Size)
+ .ThenBy(i => i.Name)
+ .ThenByDescending(i => i.DateModified)
+ .ThenByDescending(i => i.DateCreated);
+ // max total size 20M
+ const int MaxSize = 20971520;
+ var sizeCounter = 0L;
+ foreach (var fontFile in fontFiles)
+ {
+ sizeCounter += fontFile.Size;
+ if (sizeCounter >= MaxSize)
+ {
+ _logger.LogWarning("Some fonts will not be sent due to size limitations");
+ yield break;
+ }
+
+ yield return fontFile;
+ }
+ }
+ else
+ {
+ _logger.LogWarning("The path of fallback font folder has not been set");
+ encodingOptions.EnableFallbackFont = false;
+ }
+ }
+
+ /// <summary>
+ /// Gets a fallback font file.
+ /// </summary>
+ /// <param name="name">The name of the fallback font file to get.</param>
+ /// <response code="200">Fallback font file retrieved.</response>
+ /// <returns>The fallback font file.</returns>
+ [HttpGet("FallbackFont/Fonts/{name}")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetFallbackFont([FromRoute, Required] string name)
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var fallbackFontPath = encodingOptions.FallbackFontPath;
+
+ if (!string.IsNullOrEmpty(fallbackFontPath))
+ {
+ var fontFile = _fileSystem.GetFiles(fallbackFontPath)
+ .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ var fileSize = fontFile?.Length;
+
+ if (fontFile != null && fileSize != null && fileSize > 0)
+ {
+ _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize);
+ return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
+ }
+ else
+ {
+ _logger.LogWarning("The selected font is null or empty");
+ }
+ }
+ else
+ {
+ _logger.LogWarning("The path of fallback font folder has not been set");
+ encodingOptions.EnableFallbackFont = false;
+ }
+
+ // returning HTTP 204 will break the SubtitlesOctopus
+ return Ok();
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index d7c81a3ab..ad64adfba 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -18,6 +20,7 @@ namespace Jellyfin.Api.Controllers
/// The suggestions controller.
/// </summary>
[Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class SuggestionsController : BaseJellyfinApiController
{
private readonly IDtoService _dtoService;
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 5157b08ae..281c0016e 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,6 +1,8 @@
using System;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -124,7 +126,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] string? excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -146,10 +148,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
- [FromQuery] string? imageTypes,
+ [FromQuery] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
[FromQuery] string? genres,
@@ -158,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index d158f6c34..49a6c386f 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -13,6 +13,7 @@ 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;
@@ -77,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -134,7 +135,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -206,7 +207,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy)
{
@@ -325,7 +326,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index df20a92b3..e10f1fe91 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -76,6 +76,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
/// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="transcodingContainer">Optional. The container to transcode to.</param>
/// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
@@ -88,23 +89,22 @@ namespace Jellyfin.Api.Controllers
/// <response code="302">Redirected to remote audio stream.</response>
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")]
- [HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
- [HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
- [FromRoute] string? container,
+ [FromQuery] string? container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
[FromQuery] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
[FromQuery] string? transcodingContainer,
[FromQuery] string? transcodingProtocol,
@@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Hls,
RequireAvc = true,
@@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+ AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = maxAudioChannels,
CopyTimestamps = true,
@@ -270,20 +270,24 @@ namespace Jellyfin.Api.Controllers
{
var deviceProfile = new DeviceProfile();
- var directPlayProfiles = new List<DirectPlayProfile>();
-
var containers = RequestHelpers.Split(container, ',', true);
-
- foreach (var cont in containers)
+ int len = containers.Length;
+ var directPlayProfiles = new DirectPlayProfile[len];
+ for (int i = 0; i < len; i++)
{
- var parts = RequestHelpers.Split(cont, ',', true);
+ var parts = RequestHelpers.Split(containers[i], '|', true);
- var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
+ var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
- directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
+ directPlayProfiles[i] = new DirectPlayProfile
+ {
+ Type = DlnaProfileType.Audio,
+ Container = parts[0],
+ AudioCodec = audioCodecs
+ };
}
- deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
+ deviceProfile.DirectPlayProfiles = directPlayProfiles;
deviceProfile.TranscodingProfiles = new[]
{
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 50bb8bb2a..0f7c25d0e 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -381,17 +381,13 @@ namespace Jellyfin.Api.Controllers
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
+ if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
{
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
- _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
}
+ await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
+
return NoContent();
}
@@ -409,7 +405,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult UpdateUserPolicy(
+ public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy)
{
@@ -447,7 +443,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.RevokeUserTokens(user.Id, currentToken);
}
- _userManager.UpdatePolicy(userId, newPolicy);
+ await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
return NoContent();
}
@@ -464,7 +460,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult UpdateUserConfiguration(
+ public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig)
{
@@ -473,7 +469,7 @@ namespace Jellyfin.Api.Controllers
return Forbid("User configuration update not allowed");
}
- _userManager.UpdateConfiguration(userId, userConfig);
+ await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
return NoContent();
}
@@ -534,6 +530,33 @@ namespace Jellyfin.Api.Controllers
return result;
}
+ /// <summary>
+ /// Gets the user based on auth token.
+ /// </summary>
+ /// <response code="200">User returned.</response>
+ /// <response code="400">Token is not owned by a user.</response>
+ /// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
+ [HttpGet("Me")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public ActionResult<UserDto> GetCurrentUser()
+ {
+ var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
+ if (userId == null)
+ {
+ return BadRequest();
+ }
+
+ var user = _userManager.GetUserById(userId.Value);
+ if (user == null)
+ {
+ return BadRequest();
+ }
+
+ return _userManager.GetUserDto(user);
+ }
+
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{
var users = _userManager.Users;
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 48262f062..a52af1781 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -272,7 +272,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 2afa878f4..d7bcf79c1 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
var baseUrlParam = string.Format(
CultureInfo.InvariantCulture,
- "\"hls{0}\"",
+ "\"hls/{0}/\"",
Path.GetFileNameWithoutExtension(outputPath));
return string.Format(
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 4ecf0407b..2c685309a 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -10,6 +10,7 @@ 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;
@@ -77,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] bool recursive = true,
[FromQuery] bool? enableImages = true)
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index e61e9c29d..85f8d789e 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Extensions
bool? enableImages,
bool? enableUserData,
int? imageTypeLimit,
- string? enableImageTypes)
+ ImageType[] enableImageTypes)
{
dtoOptions.EnableImages = enableImages ?? true;
@@ -140,11 +140,9 @@ namespace Jellyfin.Api.Extensions
dtoOptions.EnableUserData = enableUserData.Value;
}
- if (!string.IsNullOrWhiteSpace(enableImageTypes))
+ if (enableImageTypes.Length != 0)
{
- dtoOptions.ImageTypes = enableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
- .ToArray();
+ dtoOptions.ImageTypes = enableImageTypes;
}
return dtoOptions;
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
index df235ced2..29e6b4193 100644
--- a/Jellyfin.Api/Helpers/ClaimHelpers.cs
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -63,6 +63,19 @@ namespace Jellyfin.Api.Helpers
public static string? GetToken(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Token);
+ /// <summary>
+ /// Gets a flag specifying whether the request is using an api key.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>The flag specifying whether the request is using an api key.</returns>
+ public static bool GetIsApiKey(in ClaimsPrincipal user)
+ {
+ var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey);
+ return !string.IsNullOrEmpty(claimValue)
+ && bool.TryParse(claimValue, out var parsedClaimValue)
+ && parsedClaimValue;
+ }
+
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
{
return user?.Identities
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index af0519ffa..ea012f837 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Helpers
return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
}
- var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
+ var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
var builder = new StringBuilder();
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 6b516977e..366301d3e 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -123,9 +123,8 @@ namespace Jellyfin.Api.Helpers
state.Dispose();
}
- await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None)
- .WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false);
- return new FileStreamResult(httpContext.Response.Body, contentType);
+ var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
finally
{
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 1207fb513..0d8315dee 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -166,7 +166,7 @@ namespace Jellyfin.Api.Helpers
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
- long? maxBitrate,
+ int? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
@@ -551,10 +551,10 @@ namespace Jellyfin.Api.Helpers
}
}
- private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
+ private int? GetMaxBitrate(int? clientMaxBitrate, User user, string ipAddress)
{
var maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
+ var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
if (remoteClientMaxBitrate <= 0)
{
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
new file mode 100644
index 000000000..824870c7e
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -0,0 +1,166 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Model.IO;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// A progressive file stream for transferring transcoded files as they are written to.
+ /// </summary>
+ public class ProgressiveFileStream : Stream
+ {
+ private readonly FileStream _fileStream;
+ private readonly TranscodingJobDto? _job;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly bool _allowAsyncFileRead;
+ private int _bytesWritten;
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+ /// </summary>
+ /// <param name="filePath">The path to the transcoded file.</param>
+ /// <param name="job">The transcoding job information.</param>
+ /// <param name="transcodingJobHelper">The transcoding job helper.</param>
+ public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper)
+ {
+ _job = job;
+ _transcodingJobHelper = transcodingJobHelper;
+ _bytesWritten = 0;
+
+ var fileOptions = FileOptions.SequentialScan;
+ _allowAsyncFileRead = false;
+
+ // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ fileOptions |= FileOptions.Asynchronous;
+ _allowAsyncFileRead = true;
+ }
+
+ _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+ }
+
+ /// <inheritdoc />
+ public override bool CanRead => _fileStream.CanRead;
+
+ /// <inheritdoc />
+ public override bool CanSeek => false;
+
+ /// <inheritdoc />
+ public override bool CanWrite => false;
+
+ /// <inheritdoc />
+ public override long Length => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ /// <inheritdoc />
+ public override void Flush()
+ {
+ _fileStream.Flush();
+ }
+
+ /// <inheritdoc />
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return _fileStream.Read(buffer, offset, count);
+ }
+
+ /// <inheritdoc />
+ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ int totalBytesRead = 0;
+ int remainingBytesToRead = count;
+
+ int newOffset = offset;
+ while (remainingBytesToRead > 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ int bytesRead;
+ if (_allowAsyncFileRead)
+ {
+ bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
+ }
+
+ remainingBytesToRead -= bytesRead;
+ newOffset += bytesRead;
+
+ if (bytesRead > 0)
+ {
+ _bytesWritten += bytesRead;
+ totalBytesRead += bytesRead;
+
+ if (_job != null)
+ {
+ _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+ }
+ }
+ else
+ {
+ // If the job is null it's a live stream and will require user action to close
+ if (_job?.HasExited ?? false)
+ {
+ break;
+ }
+
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ return totalBytesRead;
+ }
+
+ /// <inheritdoc />
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ try
+ {
+ if (disposing)
+ {
+ _fileStream.Dispose();
+
+ if (_job != null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(_job);
+ }
+ }
+ }
+ finally
+ {
+ _disposed = true;
+ base.Dispose(disposing);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 8dcf08af5..49632dd01 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,11 +1,14 @@
using System;
-using System.Collections.Generic;
using System.Linq;
-using System.Net;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
@@ -57,18 +60,6 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
- /// Get parsed filters.
- /// </summary>
- /// <param name="filters">The filters.</param>
- /// <returns>Item filters.</returns>
- public static IEnumerable<ItemFilter> GetFilters(string? filters)
- {
- return string.IsNullOrEmpty(filters)
- ? Array.Empty<ItemFilter>()
- : filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
- }
-
- /// <summary>
/// Splits a string at a separating character into an array of substrings.
/// </summary>
/// <param name="value">The string to split.</param>
@@ -173,5 +164,67 @@ namespace Jellyfin.Api.Helpers
.Select(i => i!.Value)
.ToArray();
}
+
+ /// <summary>
+ /// Gets the item fields.
+ /// </summary>
+ /// <param name="imageTypes">The image types string.</param>
+ /// <returns>IEnumerable{ItemFields}.</returns>
+ internal static ImageType[] GetImageTypes(string? imageTypes)
+ {
+ if (string.IsNullOrEmpty(imageTypes))
+ {
+ return Array.Empty<ImageType>();
+ }
+
+ return Split(imageTypes, ',', true)
+ .Select(v =>
+ {
+ if (Enum.TryParse(v, true, out ImageType value))
+ {
+ return (ImageType?)value;
+ }
+
+ return null;
+ })
+ .Where(i => i.HasValue)
+ .Select(i => i!.Value)
+ .ToArray();
+ }
+
+ internal static QueryResult<BaseItemDto> CreateQueryResult(
+ QueryResult<(BaseItem, ItemCounts)> result,
+ DtoOptions dtoOptions,
+ IDtoService dtoService,
+ bool includeItemTypes,
+ User? user)
+ {
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, counts) = i;
+ var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (includeItemTypes)
+ {
+ dto.ChildCount = counts.ItemCount;
+ dto.ProgramCount = counts.ProgramCount;
+ dto.SeriesCount = counts.SeriesCount;
+ dto.EpisodeCount = counts.EpisodeCount;
+ dto.MovieCount = counts.MovieCount;
+ dto.TrailerCount = counts.TrailerCount;
+ dto.AlbumCount = counts.AlbumCount;
+ dto.SongCount = counts.SongCount;
+ dto.ArtistCount = counts.ArtistCount;
+ }
+
+ return dto;
+ });
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
}
}
diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
index b922e76cf..6b06f87cd 100644
--- a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
+++ b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
@@ -50,9 +50,9 @@ namespace Jellyfin.Api.Helpers
var returnItems = items;
- if (limit.HasValue)
+ if (limit.HasValue && limit < returnItems.Count)
{
- returnItems = returnItems.Take(limit.Value).ToList();
+ returnItems = returnItems.GetRange(0, limit.Value);
}
var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 64d1227f7..0db1fabff 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -740,10 +740,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="state">The state.</param>
private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
{
- if (job != null)
- {
- job.HasExited = true;
- }
+ job.HasExited = true;
_logger.LogDebug("Disposing stream resources");
state.Dispose();
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 6a00db4b1..da0852ceb 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -14,9 +14,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.8" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.9" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
</ItemGroup>
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
new file mode 100644
index 000000000..4f012cab2
--- /dev/null
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
@@ -0,0 +1,59 @@
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.ModelBinders
+{
+ /// <summary>
+ /// Comma delimited array model binder.
+ /// Returns an empty array of specified type if there is no query parameter.
+ /// </summary>
+ public class CommaDelimitedArrayModelBinder : IModelBinder
+ {
+ /// <inheritdoc/>
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
+ var converter = TypeDescriptor.GetConverter(elementType);
+
+ if (valueProviderResult.Length > 1)
+ {
+ var result = Array.CreateInstance(elementType, valueProviderResult.Length);
+
+ for (int i = 0; i < valueProviderResult.Length; i++)
+ {
+ var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim());
+
+ result.SetValue(value, i);
+ }
+
+ bindingContext.Result = ModelBindingResult.Success(result);
+ }
+ else
+ {
+ var value = valueProviderResult.FirstValue;
+
+ if (value != null)
+ {
+ var values = Array.ConvertAll(
+ value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries),
+ x => converter.ConvertFromString(x?.Trim()));
+
+ var typedValues = Array.CreateInstance(elementType, values.Length);
+ values.CopyTo(typedValues, 0);
+
+ bindingContext.Result = ModelBindingResult.Success(typedValues);
+ }
+ else
+ {
+ var emptyResult = Array.CreateInstance(elementType, 0);
+ bindingContext.Result = ModelBindingResult.Success(emptyResult);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index d7eaab30d..581153393 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -1,4 +1,8 @@
using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Model.Entities;
namespace Jellyfin.Api.Models.LiveTvDtos
{
@@ -137,7 +141,9 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// Gets or sets the image types to include in the output.
/// Optional.
/// </summary>
- public string? EnableImageTypes { get; set; }
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "EnableImageTypes", Justification = "Imported from ServiceStack")]
+ public ImageType[] EnableImageTypes { get; set; } = Array.Empty<ImageType>();
/// <summary>
/// Gets or sets include user data.
diff --git a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
new file mode 100644
index 000000000..30473255e
--- /dev/null
+++ b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
@@ -0,0 +1,34 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.SubtitleDtos
+{
+ /// <summary>
+ /// Upload subtitles dto.
+ /// </summary>
+ public class UploadSubtitleDto
+ {
+ /// <summary>
+ /// Gets or sets the subtitle language.
+ /// </summary>
+ [Required]
+ public string Language { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the subtitle format.
+ /// </summary>
+ [Required]
+ public string Format { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the subtitle is forced.
+ /// </summary>
+ [Required]
+ public bool IsForced { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle data.
+ /// </summary>
+ [Required]
+ public string Data { get; set; } = string.Empty;
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 849b3b709..77d55828d 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.WebSocketListeners
@@ -29,11 +30,14 @@ namespace Jellyfin.Api.WebSocketListeners
_activityManager.EntryCreated += OnEntryCreated;
}
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- protected override string Name => "ActivityLogEntry";
+ /// <inheritdoc />
+ protected override SessionMessageType Type => SessionMessageType.ActivityLogEntry;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StartType => SessionMessageType.ActivityLogEntryStart;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StopType => SessionMessageType.ActivityLogEntryStop;
/// <summary>
/// Gets the data to send.
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 8a966c137..80314b923 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -33,11 +34,14 @@ namespace Jellyfin.Api.WebSocketListeners
_taskManager.TaskCompleted += OnTaskCompleted;
}
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- protected override string Name => "ScheduledTasksInfo";
+ /// <inheritdoc />
+ protected override SessionMessageType Type => SessionMessageType.ScheduledTasksInfo;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StartType => SessionMessageType.ScheduledTasksInfoStart;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StopType => SessionMessageType.ScheduledTasksInfoStop;
/// <summary>
/// Gets the data to send.
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 1fb5dc412..1cf43a005 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.WebSocketListeners
@@ -34,7 +35,13 @@ namespace Jellyfin.Api.WebSocketListeners
}
/// <inheritdoc />
- protected override string Name => "Sessions";
+ protected override SessionMessageType Type => SessionMessageType.Sessions;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StartType => SessionMessageType.SessionsStart;
+
+ /// <inheritdoc />
+ protected override SessionMessageType StopType => SessionMessageType.SessionsStop;
/// <summary>
/// Gets the data to send.
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index f7ab57a1b..6d4681914 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -189,6 +189,11 @@ namespace Jellyfin.Data.Entities
public int? LoginAttemptsBeforeLockout { get; set; }
/// <summary>
+ /// Gets or sets the maximum number of active sessions the user can have at once.
+ /// </summary>
+ public int MaxActiveSessions { get; set; }
+
+ /// <summary>
/// Gets or sets the subtitle mode.
/// </summary>
/// <remarks>
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 6bb0d8ce2..5038988f9 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -41,8 +41,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.8" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.8" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.9" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs
new file mode 100644
index 000000000..92919d3a5
--- /dev/null
+++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Jellyfin.Data.Queries
+{
+ /// <summary>
+ /// A class representing a query to the activity logs.
+ /// </summary>
+ public class ActivityLogQuery
+ {
+ /// <summary>
+ /// Gets or sets the index to start at.
+ /// </summary>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum number of items to include.
+ /// </summary>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to take entries with a user id.
+ /// </summary>
+ public bool? HasUserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum date to query for.
+ /// </summary>
+ public DateTime? MinDate { get; set; }
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index a1caa751b..6a9dbdae4 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -553,13 +553,13 @@ namespace Jellyfin.Drawing.Skia
}
/// <inheritdoc/>
- public void CreateImageCollage(ImageCollageOptions options)
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
double ratio = (double)options.Width / options.Height;
if (ratio >= 1.4)
{
- new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+ new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
}
else if (ratio >= .9)
{
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 10bb59648..0e94f87f6 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -82,48 +82,62 @@ namespace Jellyfin.Drawing.Skia
/// <param name="outputPath">The path at which to place the resulting image.</param>
/// <param name="width">The desired width of the collage.</param>
/// <param name="height">The desired height of the collage.</param>
- public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
+ /// <param name="libraryName">The name of the library to draw on the collage.</param>
+ public void BuildThumbCollage(string[] paths, string outputPath, int width, int height, string? libraryName)
{
- using var bitmap = BuildThumbCollageBitmap(paths, width, height);
+ using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
using var outputStream = new SKFileWStream(outputPath);
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
- private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
+ private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height, string? libraryName)
{
var bitmap = new SKBitmap(width, height);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Black);
- // number of images used in the thumbnail
- var iCount = 3;
-
- // determine sizes for each image that will composited into the final image
- var iSlice = Convert.ToInt32(width / iCount);
- int iHeight = Convert.ToInt32(height * 1.00);
- int imageIndex = 0;
- for (int i = 0; i < iCount; i++)
+ using var backdrop = GetNextValidImage(paths, 0, out _);
+ if (backdrop == null)
{
- using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
- imageIndex = newIndex;
- if (currentBitmap == null)
- {
- continue;
- }
+ return bitmap;
+ }
- // resize to the same aspect as the original
- int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
- using var resizedImage = SkiaEncoder.ResizeImage(currentBitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
+ // resize to the same aspect as the original
+ var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
+ using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
+ // draw the backdrop
+ canvas.DrawImage(residedBackdrop, 0, 0);
- // crop image
- int ix = Math.Abs((iWidth - iSlice) / 2);
- using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
- // draw image onto canvas
- canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0);
+ // draw shadow rectangle
+ var paintColor = new SKPaint
+ {
+ Color = SKColors.Black.WithAlpha(0x78),
+ Style = SKPaintStyle.Fill
+ };
+ canvas.DrawRect(0, 0, width, height, paintColor);
+
+ // draw library name
+ var textPaint = new SKPaint
+ {
+ Color = SKColors.White,
+ Style = SKPaintStyle.Fill,
+ TextSize = 112,
+ TextAlign = SKTextAlign.Center,
+ Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright),
+ IsAntialias = true
+ };
+
+ // scale down text to 90% of the width if text is larger than 95% of the width
+ var textWidth = textPaint.MeasureText(libraryName);
+ if (textWidth > width * 0.95)
+ {
+ textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
}
+ canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+
return bitmap;
}
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index abdd290d4..7bde4f35b 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -3,8 +3,10 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
+using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Activity
{
@@ -39,39 +41,47 @@ namespace Jellyfin.Server.Implementations.Activity
}
/// <inheritdoc/>
- public QueryResult<ActivityLogEntry> GetPagedResult(
- Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
- int? startIndex,
- int? limit)
+ public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
{
- using var dbContext = _provider.CreateContext();
+ await using var dbContext = _provider.CreateContext();
- var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated));
+ IQueryable<ActivityLog> entries = dbContext.ActivityLogs
+ .AsQueryable()
+ .OrderByDescending(entry => entry.DateCreated);
- if (startIndex.HasValue)
+ if (query.MinDate.HasValue)
{
- query = query.Skip(startIndex.Value);
+ entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
}
- if (limit.HasValue)
+ if (query.HasUserId.HasValue)
{
- query = query.Take(limit.Value);
+ entries = entries.Where(entry => entry.UserId != Guid.Empty == query.HasUserId.Value );
}
- // This converts the objects from the new database model to the old for compatibility with the existing API.
- var list = query.Select(ConvertToOldModel).ToList();
-
return new QueryResult<ActivityLogEntry>
{
- Items = list,
- TotalRecordCount = func(dbContext.ActivityLogs).Count()
+ Items = await entries
+ .Skip(query.StartIndex ?? 0)
+ .Take(query.Limit ?? 100)
+ .AsAsyncEnumerable()
+ .Select(ConvertToOldModel)
+ .ToListAsync()
+ .ConfigureAwait(false),
+ TotalRecordCount = await entries.CountAsync().ConfigureAwait(false)
};
}
- /// <inheritdoc/>
- public QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit)
+ /// <inheritdoc />
+ public async Task CleanAsync(DateTime startDate)
{
- return GetPagedResult(logs => logs, startIndex, limit);
+ await using var dbContext = _provider.CreateContext();
+ var entries = dbContext.ActivityLogs
+ .AsQueryable()
+ .Where(entry => entry.DateCreated <= startDate);
+
+ dbContext.RemoveRange(entries);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
index 80ed56cd8..0993c6df7 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
namespace Jellyfin.Server.Implementations.Events.Consumers.System
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
/// <inheritdoc />
public async Task OnEvent(TaskCompletionEventArgs eventArgs)
{
- await _sessionManager.SendMessageToAdminSessions("ScheduledTaskEnded", eventArgs.Result, CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendMessageToAdminSessions(SessionMessageType.ScheduledTaskEnded, eventArgs.Result, CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
index 1c600683a..1d790da6b 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginInstallationCancelledEventArgs eventArgs)
{
- await _sessionManager.SendMessageToAdminSessions("PackageInstallationCancelled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCancelled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
index ea0c878d4..a1faf18fc 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(InstallationFailedEventArgs eventArgs)
{
- await _sessionManager.SendMessageToAdminSessions("PackageInstallationFailed", eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationFailed, eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
index 3dda5a04c..bd1a71404 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginInstalledEventArgs eventArgs)
{
- await _sessionManager.SendMessageToAdminSessions("PackageInstallationCompleted", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCompleted, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
index f691d11a7..b513ac64a 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginInstallingEventArgs eventArgs)
{
- await _sessionManager.SendMessageToAdminSessions("PackageInstalling", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstalling, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
index 709692f6b..1fd7b9adf 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginUninstalledEventArgs eventArgs)
{
- await _sessionManager.SendMessageToAdminSessions("PluginUninstalled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageUninstalled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
index 10367a939..303e88621 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events.Users;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
@@ -30,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
await _sessionManager.SendMessageToUserSessions(
new List<Guid> { eventArgs.Argument.Id },
- "UserDeleted",
+ SessionMessageType.UserDeleted,
eventArgs.Argument.Id.ToString("N", CultureInfo.InvariantCulture),
CancellationToken.None).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
index 6081dd044..a14911b94 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
@@ -6,6 +6,7 @@ using Jellyfin.Data.Events.Users;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
@@ -33,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
await _sessionManager.SendMessageToUserSessions(
new List<Guid> { e.Argument.Id },
- "UserUpdated",
+ SessionMessageType.UserUpdated,
_userManager.GetUserDto(e.Argument),
CancellationToken.None).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 4e79dd8d6..c52be3b8a 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -24,11 +24,12 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.8">
+ <PackageReference Include="System.Linq.Async" Version="4.1.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.8">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
new file mode 100644
index 000000000..e5c326a32
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
@@ -0,0 +1,464 @@
+#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("20201004171403_AddMaxActiveSessions")]
+ partial class AddMaxActiveSessions
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "3.1.8");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<string>("DashboardTheme")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("UserId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(64);
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Permission_Permissions_Guid");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Preference_Preferences_Guid");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("EasyPassword")
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("DisplayPreferences")
+ .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("Permission_Permissions_Guid");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("Preference_Preferences_Guid");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs
new file mode 100644
index 000000000..10acb4def
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddMaxActiveSessions : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<int>(
+ name: "MaxActiveSessions",
+ schema: "jellyfin",
+ table: "Users",
+ nullable: false,
+ defaultValue: 0);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "MaxActiveSessions",
+ schema: "jellyfin",
+ table: "Users");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index ccfcf96b1..16d62f482 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "3.1.7");
+ .HasAnnotation("ProductVersion", "3.1.8");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -344,6 +344,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index 46f1c618f..76f943385 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -61,6 +61,7 @@ namespace Jellyfin.Server.Implementations.Users
public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
{
return _dbContext.ItemDisplayPreferences
+ .AsQueryable()
.Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
.ToList();
}
diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
index e38cd07f0..5f32479e1 100644
--- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Users
public string Name => "InvalidOrMissingAuthenticationProvider";
/// <inheritdoc />
- public bool IsEnabled => true;
+ public bool IsEnabled => false;
/// <inheritdoc />
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 8f04baa08..40b89ed28 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -2,6 +2,7 @@
#pragma warning disable CA1307
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -48,6 +49,8 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
+ private readonly IDictionary<Guid, User> _users;
+
/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary>
@@ -81,37 +84,28 @@ namespace Jellyfin.Server.Implementations.Users
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
+
+ _users = new ConcurrentDictionary<Guid, User>();
+ using var dbContext = _dbProvider.CreateContext();
+ foreach (var user in dbContext.Users
+ .Include(user => user.Permissions)
+ .Include(user => user.Preferences)
+ .Include(user => user.AccessSchedules)
+ .Include(user => user.ProfileImage)
+ .AsEnumerable())
+ {
+ _users.Add(user.Id, user);
+ }
}
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/>
- public IEnumerable<User> Users
- {
- get
- {
- using var dbContext = _dbProvider.CreateContext();
- return dbContext.Users
- .Include(user => user.Permissions)
- .Include(user => user.Preferences)
- .Include(user => user.AccessSchedules)
- .Include(user => user.ProfileImage)
- .ToList();
- }
- }
+ public IEnumerable<User> Users => _users.Values;
/// <inheritdoc/>
- public IEnumerable<Guid> UsersIds
- {
- get
- {
- using var dbContext = _dbProvider.CreateContext();
- return dbContext.Users
- .Select(user => user.Id)
- .ToList();
- }
- }
+ public IEnumerable<Guid> UsersIds => _users.Keys;
/// <inheritdoc/>
public User? GetUserById(Guid id)
@@ -121,13 +115,8 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- using var dbContext = _dbProvider.CreateContext();
- return dbContext.Users
- .Include(user => user.Permissions)
- .Include(user => user.Preferences)
- .Include(user => user.AccessSchedules)
- .Include(user => user.ProfileImage)
- .FirstOrDefault(user => user.Id == id);
+ _users.TryGetValue(id, out var user);
+ return user;
}
/// <inheritdoc/>
@@ -138,14 +127,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name));
}
- using var dbContext = _dbProvider.CreateContext();
- return dbContext.Users
- .Include(user => user.Permissions)
- .Include(user => user.Preferences)
- .Include(user => user.AccessSchedules)
- .Include(user => user.ProfileImage)
- .AsEnumerable()
- .FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
+ return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
}
/// <inheritdoc/>
@@ -200,17 +182,21 @@ namespace Jellyfin.Server.Implementations.Users
internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
{
// TODO: Remove after user item data is migrated.
- var max = await dbContext.Users.AnyAsync().ConfigureAwait(false)
- ? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
+ var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
+ ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
: 0;
- return new User(
+ var user = new User(
name,
_defaultAuthenticationProvider.GetType().FullName,
_defaultPasswordResetProvider.GetType().FullName)
{
InternalId = max + 1
};
+
+ _users.Add(user.Id, user);
+
+ return user;
}
/// <inheritdoc/>
@@ -221,7 +207,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
- using var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
@@ -236,28 +222,12 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void DeleteUser(Guid userId)
{
- using var dbContext = _dbProvider.CreateContext();
- var user = dbContext.Users
- .Include(u => u.Permissions)
- .Include(u => u.Preferences)
- .Include(u => u.AccessSchedules)
- .Include(u => u.ProfileImage)
- .FirstOrDefault(u => u.Id == userId);
- if (user == null)
+ if (!_users.TryGetValue(userId, out var user))
{
throw new ResourceNotFoundException(nameof(userId));
}
- 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)
+ if (_users.Count == 1)
{
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
@@ -276,6 +246,8 @@ namespace Jellyfin.Server.Implementations.Users
nameof(userId));
}
+ using var dbContext = _dbProvider.CreateContext();
+
// Clear all entities related to the user from the database.
if (user.ProfileImage != null)
{
@@ -287,6 +259,7 @@ namespace Jellyfin.Server.Implementations.Users
dbContext.RemoveRange(user.AccessSchedules);
dbContext.Users.Remove(user);
dbContext.SaveChanges();
+ _users.Remove(userId);
_eventManager.Publish(new UserDeletedEventArgs(user));
}
@@ -379,6 +352,7 @@ namespace Jellyfin.Server.Implementations.Users
PasswordResetProviderId = user.PasswordResetProviderId,
InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
+ MaxActiveSessions = user.MaxActiveSessions,
IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
IsHidden = user.HasPermission(PermissionKind.IsHidden),
IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
@@ -458,11 +432,9 @@ namespace Jellyfin.Server.Implementations.Users
// the authentication provider might have created it
user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
- if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
+ if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user != null)
{
- UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy());
-
- await UpdateUserAsync(user).ConfigureAwait(false);
+ await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
}
}
}
@@ -587,9 +559,7 @@ namespace Jellyfin.Server.Implementations.Users
public async Task InitializeAsync()
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
- using var dbContext = _dbProvider.CreateContext();
-
- if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
+ if (_users.Any())
{
return;
}
@@ -602,6 +572,7 @@ namespace Jellyfin.Server.Implementations.Users
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
+ await using var dbContext = _dbProvider.CreateContext();
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@@ -642,9 +613,9 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void UpdateConfiguration(Guid userId, UserConfiguration config)
+ public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
{
- using var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
@@ -671,13 +642,13 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user);
- dbContext.SaveChanges();
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
- public void UpdatePolicy(Guid userId, UserPolicy policy)
+ public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
{
- using var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
@@ -701,6 +672,7 @@ namespace Jellyfin.Server.Implementations.Users
user.PasswordResetProviderId = policy.PasswordResetProviderId;
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
user.LoginAttemptsBeforeLockout = maxLoginAttempts;
+ user.MaxActiveSessions = policy.MaxActiveSessions;
user.SyncPlayAccess = policy.SyncPlayAccess;
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
@@ -741,15 +713,16 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
- dbContext.SaveChanges();
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
- public void ClearProfileImage(User user)
+ public async Task ClearProfileImageAsync(User user)
{
- using var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
dbContext.Remove(user.ProfileImage);
- dbContext.SaveChanges();
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ user.ProfileImage = null;
}
private static bool IsValidUsername(string name)
@@ -799,7 +772,7 @@ namespace Jellyfin.Server.Implementations.Users
private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
{
- var passwordResetProviderId = user?.PasswordResetProviderId;
+ var passwordResetProviderId = user.PasswordResetProviderId;
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
if (!string.IsNullOrEmpty(passwordResetProviderId))
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 8d569a779..c44736447 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -4,6 +4,8 @@ using System.IO;
using System.Reflection;
using Emby.Drawing;
using Emby.Server.Implementations;
+using Emby.Server.Implementations.Session;
+using Jellyfin.Api.WebSocketListeners;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
@@ -14,6 +16,7 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
@@ -80,6 +83,14 @@ namespace Jellyfin.Server
ServiceCollection.AddSingleton<IUserManager, UserManager>();
ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+ ServiceCollection.AddScoped<IWebSocketListener, SessionWebSocketListener>();
+ ServiceCollection.AddScoped<IWebSocketListener, ActivityLogWebSocketListener>();
+ ServiceCollection.AddScoped<IWebSocketListener, ScheduledTasksWebSocketListener>();
+ ServiceCollection.AddScoped<IWebSocketListener, SessionInfoWebSocketListener>();
+
+ // TODO fix circular dependency on IWebSocketManager
+ ServiceCollection.AddScoped(serviceProvider => new Lazy<IEnumerable<IWebSocketListener>>(serviceProvider.GetRequiredService<IEnumerable<IWebSocketListener>>));
+
base.RegisterServices();
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 5bcf6d5f0..cc98955df 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
+using Emby.Server.Implementations;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.DownloadPolicy;
@@ -27,6 +28,8 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
@@ -209,7 +212,19 @@ namespace Jellyfin.Server.Extensions
{
return serviceCollection.AddSwaggerGen(c =>
{
- c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
+ c.SwaggerDoc("api-docs", new OpenApiInfo
+ {
+ Title = "Jellyfin API",
+ Version = "v1",
+ Extensions = new Dictionary<string, IOpenApiExtension>
+ {
+ {
+ "x-jellyfin-version",
+ new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString())
+ }
+ }
+ });
+
c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
@@ -260,6 +275,7 @@ namespace Jellyfin.Server.Extensions
c.AddSwaggerTypeMappings();
c.OperationFilter<FileResponseFilter>();
+ c.DocumentFilter<WebsocketModelFilter>();
});
}
diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs
new file mode 100644
index 000000000..248802857
--- /dev/null
+++ b/Jellyfin.Server/Filters/WebsocketModelFilter.cs
@@ -0,0 +1,30 @@
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Jellyfin.Server.Filters
+{
+ /// <summary>
+ /// Add models used in websocket messaging.
+ /// </summary>
+ public class WebsocketModelFilter : IDocumentFilter
+ {
+ /// <inheritdoc />
+ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+ {
+ context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(PlaystateRequest), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
+
+ context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 761a92f6d..a64d2e1cd 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -38,12 +38,12 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.8" />
- <PackageReference Include="prometheus-net" Version="3.6.0" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" />
+ <PackageReference Include="prometheus-net" Version="4.0.0" />
+ <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index fb1ee3b2b..f6c76e4d9 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -125,8 +125,8 @@ namespace Jellyfin.Server.Middleware
switch (ex)
{
case ArgumentException _: return StatusCodes.Status400BadRequest;
- case AuthenticationException _:
- case SecurityException _: return StatusCodes.Status401Unauthorized;
+ case AuthenticationException _: return StatusCodes.Status401Unauthorized;
+ case SecurityException _: return StatusCodes.Status403Forbidden;
case DirectoryNotFoundException _:
case FileNotFoundException _:
case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index c933d679f..97a51c202 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -290,23 +290,19 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Kestrel listening on {IpAddress}", address);
options.Listen(address, appHost.HttpPort);
+
if (appHost.ListenWithHttps)
{
- options.Listen(address, appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps(appHost.Certificate);
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.Listen(
+ address,
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps(appHost.Certificate));
}
else if (builderContext.HostingEnvironment.IsDevelopment())
{
try
{
- options.Listen(address, appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps();
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.Listen(address, appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
}
catch (InvalidOperationException ex)
{
@@ -322,21 +318,15 @@ namespace Jellyfin.Server
if (appHost.ListenWithHttps)
{
- options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps(appHost.Certificate);
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.ListenAnyIP(
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps(appHost.Certificate));
}
else if (builderContext.HostingEnvironment.IsDevelopment())
{
try
{
- options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
- {
- listenOptions.UseHttps();
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.ListenAnyIP(appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
}
catch (InvalidOperationException ex)
{
@@ -378,7 +368,7 @@ namespace Jellyfin.Server
.ConfigureServices(services =>
{
// Merge the external ServiceCollection into ASP.NET DI
- services.TryAdd(serviceCollection);
+ services.Add(serviceCollection);
})
.UseStartup<Startup>();
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 2f4620aa6..62ffe174c 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.Net.Http.Headers;
+using System.Net.Mime;
using Jellyfin.Api.TypeConverters;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
@@ -11,6 +12,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@@ -123,10 +125,15 @@ namespace Jellyfin.Server
mainApp.UseStaticFiles();
if (appConfig.HostWebClient())
{
+ var extensionProvider = new FileExtensionContentTypeProvider();
+
+ // subtitles octopus requires .data files.
+ extensionProvider.Mappings.Add(".data", MediaTypeNames.Application.Octet);
mainApp.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath),
- RequestPath = "/web"
+ RequestPath = "/web",
+ ContentTypeProvider = extensionProvider
});
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
new file mode 100644
index 000000000..bf7048c37
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
@@ -0,0 +1,53 @@
+using System;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Convert comma delimited string to array of type.
+ /// </summary>
+ /// <typeparam name="T">Type to convert to.</typeparam>
+ public class JsonCommaDelimitedArrayConverter<T> : JsonConverter<T[]>
+ {
+ private readonly TypeConverter _typeConverter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class.
+ /// </summary>
+ public JsonCommaDelimitedArrayConverter()
+ {
+ _typeConverter = TypeDescriptor.GetConverter(typeof(T));
+ }
+
+ /// <inheritdoc />
+ public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var stringEntries = reader.GetString()?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ if (stringEntries == null || stringEntries.Length == 0)
+ {
+ return Array.Empty<T>();
+ }
+
+ var entries = new T[stringEntries.Length];
+ for (var i = 0; i < stringEntries.Length; i++)
+ {
+ entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i].Trim());
+ }
+
+ return entries;
+ }
+
+ return JsonSerializer.Deserialize<T[]>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, options);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
new file mode 100644
index 000000000..24ed3ea19
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Json comma delimited array converter factory.
+ /// </summary>
+ /// <remarks>
+ /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
+ /// </remarks>
+ public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory
+ {
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
+ return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs
index cffc41ba3..0501f7b2a 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs
@@ -8,37 +8,38 @@ namespace MediaBrowser.Common.Json.Converters
/// Converts a nullable struct or value to/from JSON.
/// Required - some clients send an empty string.
/// </summary>
- /// <typeparam name="T">The struct type.</typeparam>
- public class JsonNullableStructConverter<T> : JsonConverter<T?>
- where T : struct
+ /// <typeparam name="TStruct">The struct type.</typeparam>
+ public class JsonNullableStructConverter<TStruct> : JsonConverter<TStruct?>
+ where TStruct : struct
{
- private readonly JsonConverter<T?> _baseJsonConverter;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="JsonNullableStructConverter{T}"/> class.
- /// </summary>
- /// <param name="baseJsonConverter">The base json converter.</param>
- public JsonNullableStructConverter(JsonConverter<T?> baseJsonConverter)
- {
- _baseJsonConverter = baseJsonConverter;
- }
-
/// <inheritdoc />
- public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- // Handle empty string.
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ // Token is empty string.
if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
{
return null;
}
- return _baseJsonConverter.Read(ref reader, typeToConvert, options);
+ return JsonSerializer.Deserialize<TStruct>(ref reader, options);
}
/// <inheritdoc />
- public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
+ public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
{
- _baseJsonConverter.Write(writer, value, options);
+ if (value.HasValue)
+ {
+ JsonSerializer.Serialize(writer, value.Value, options);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
new file mode 100644
index 000000000..d5b54e3ca
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Json nullable struct converter factory.
+ /// </summary>
+ public class JsonNullableStructConverterFactory : JsonConverterFactory
+ {
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeToConvert.IsGenericType
+ && typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>)
+ && typeToConvert.GenericTypeArguments[0].IsValueType;
+ }
+
+ /// <inheritdoc />
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ var structType = typeToConvert.GenericTypeArguments[0];
+ return (JsonConverter)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 67f7e8f14..6605ae962 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -39,14 +39,9 @@ namespace MediaBrowser.Common.Json
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
- // Get built-in converters for fallback converting.
- var baseNullableInt32Converter = (JsonConverter<int?>)options.GetConverter(typeof(int?));
- var baseNullableInt64Converter = (JsonConverter<long?>)options.GetConverter(typeof(long?));
-
options.Converters.Add(new JsonGuidConverter());
options.Converters.Add(new JsonStringEnumConverter());
- options.Converters.Add(new JsonNullableStructConverter<int>(baseNullableInt32Converter));
- options.Converters.Add(new JsonNullableStructConverter<long>(baseNullableInt64Converter));
+ options.Converters.Add(new JsonNullableStructConverterFactory());
return options;
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 322740cca..e716a6610 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -18,8 +18,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.9" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
index 4b2918d08..8545fd5dc 100644
--- a/MediaBrowser.Common/Plugins/BasePlugin.cs
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Reflection;
+using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
@@ -140,6 +141,30 @@ namespace MediaBrowser.Common.Plugins
{
ApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
+ if (this is IPluginAssembly assemblyPlugin)
+ {
+ var assembly = GetType().Assembly;
+ var assemblyName = assembly.GetName();
+ var assemblyFilePath = assembly.Location;
+
+ var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
+
+ assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
+
+ var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
+ if (idAttributes.Length > 0)
+ {
+ var attribute = (GuidAttribute)idAttributes[0];
+ var assemblyId = new Guid(attribute.Value);
+
+ assemblyPlugin.SetId(assemblyId);
+ }
+ }
+
+ if (this is IHasPluginConfiguration hasPluginConfiguration)
+ {
+ hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
+ }
}
/// <summary>
diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs
new file mode 100644
index 000000000..7927c663d
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace MediaBrowser.Common.Plugins
+{
+ /// <summary>
+ /// Local plugin struct.
+ /// </summary>
+ public class LocalPlugin : IEquatable<LocalPlugin>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LocalPlugin"/> class.
+ /// </summary>
+ /// <param name="id">The plugin id.</param>
+ /// <param name="name">The plugin name.</param>
+ /// <param name="version">The plugin version.</param>
+ /// <param name="path">The plugin path.</param>
+ public LocalPlugin(Guid id, string name, Version version, string path)
+ {
+ Id = id;
+ Name = name;
+ Version = version;
+ Path = path;
+ DllFiles = new List<string>();
+ }
+
+ /// <summary>
+ /// Gets the plugin id.
+ /// </summary>
+ public Guid Id { get; }
+
+ /// <summary>
+ /// Gets the plugin name.
+ /// </summary>
+ public string Name { get; }
+
+ /// <summary>
+ /// Gets the plugin version.
+ /// </summary>
+ public Version Version { get; }
+
+ /// <summary>
+ /// Gets the plugin path.
+ /// </summary>
+ public string Path { get; }
+
+ /// <summary>
+ /// Gets the list of dll files for this plugin.
+ /// </summary>
+ public List<string> DllFiles { get; }
+
+ /// <summary>
+ /// == operator.
+ /// </summary>
+ /// <param name="left">Left item.</param>
+ /// <param name="right">Right item.</param>
+ /// <returns>Comparison result.</returns>
+ public static bool operator ==(LocalPlugin left, LocalPlugin right)
+ {
+ return left.Equals(right);
+ }
+
+ /// <summary>
+ /// != operator.
+ /// </summary>
+ /// <param name="left">Left item.</param>
+ /// <param name="right">Right item.</param>
+ /// <returns>Comparison result.</returns>
+ public static bool operator !=(LocalPlugin left, LocalPlugin right)
+ {
+ return !left.Equals(right);
+ }
+
+ /// <summary>
+ /// Compare two <see cref="LocalPlugin"/>.
+ /// </summary>
+ /// <param name="a">The first item.</param>
+ /// <param name="b">The second item.</param>
+ /// <returns>Comparison result.</returns>
+ public static int Compare(LocalPlugin a, LocalPlugin b)
+ {
+ var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+ // Id is not equal but name is.
+ if (a.Id != b.Id && compare == 0)
+ {
+ compare = a.Id.CompareTo(b.Id);
+ }
+
+ return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
+ }
+
+ /// <inheritdoc />
+ public override bool Equals(object obj)
+ {
+ return obj is LocalPlugin other && this.Equals(other);
+ }
+
+ /// <inheritdoc />
+ public override int GetHashCode()
+ {
+ return Name.GetHashCode(StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <inheritdoc />
+ public bool Equals(LocalPlugin other)
+ {
+ return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
+ && Id.Equals(other.Id);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index 169aca2ca..6aa16fea7 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -11,29 +11,6 @@ namespace MediaBrowser.Common.Updates
{
public interface IInstallationManager : IDisposable
{
- event EventHandler<InstallationInfo> PackageInstalling;
-
- event EventHandler<InstallationInfo> PackageInstallationCompleted;
-
- event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-
- event EventHandler<InstallationInfo> PackageInstallationCancelled;
-
- /// <summary>
- /// Occurs when a plugin is uninstalled.
- /// </summary>
- event EventHandler<IPlugin> PluginUninstalled;
-
- /// <summary>
- /// Occurs when a plugin is updated.
- /// </summary>
- event EventHandler<InstallationInfo> PluginUpdated;
-
- /// <summary>
- /// Occurs when a plugin is installed.
- /// </summary>
- event EventHandler<InstallationInfo> PluginInstalled;
-
/// <summary>
/// Gets the completed installations.
/// </summary>
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index f9b2e6fef..770c6dc2d 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#nullable enable
using System;
using System.Collections.Generic;
@@ -63,6 +64,7 @@ namespace MediaBrowser.Controller.Drawing
/// Create an image collage.
/// </summary>
/// <param name="options">The options to use when creating the collage.</param>
- void CreateImageCollage(ImageCollageOptions options);
+ /// <param name="libraryName">Optional. </param>
+ void CreateImageCollage(ImageCollageOptions options, string? libraryName);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index b7edb1052..935a79031 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#nullable enable
using System;
using System.Collections.Generic;
@@ -75,7 +76,7 @@ namespace MediaBrowser.Controller.Drawing
/// </summary>
/// <param name="options">The options.</param>
/// <returns>Task.</returns>
- Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
+ Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
/// <summary>
/// Gets the supported image output formats.
@@ -87,7 +88,8 @@ namespace MediaBrowser.Controller.Drawing
/// Creates the image collage.
/// </summary>
/// <param name="options">The options.</param>
- void CreateImageCollage(ImageCollageOptions options);
+ /// <param name="libraryName">The library name to draw onto the collage.</param>
+ void CreateImageCollage(ImageCollageOptions options, string? libraryName);
bool SupportsTransparency(string path);
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 397a68ff7..c5e50cf45 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.Entities.Audio
{
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) };
query.ArtistIds = new[] { Id };
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index 5a117a6b1..f0c076108 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
- query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) };
return LibraryManager.GetItemList(query);
}
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index 8a69971d0..c65477d39 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -45,7 +45,8 @@ namespace MediaBrowser.Controller.Entities
{
if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
{
- item.SetImage(new ItemImageInfo
+ item.SetImage(
+ new ItemImageInfo
{
Path = file,
Type = imageType
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index db59cdc31..5f94b5a66 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -267,7 +267,8 @@ namespace MediaBrowser.Controller.Entities
var id = child.Id;
if (dictionary.ContainsKey(id))
{
- Logger.LogError("Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}",
+ Logger.LogError(
+ "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}",
Path ?? Name,
child.Path ?? child.Name);
}
@@ -754,7 +755,7 @@ namespace MediaBrowser.Controller.Entities
private bool RequiresPostFiltering2(InternalItemsQuery query)
{
- if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase))
{
Logger.LogDebug("Query requires post-filtering due to BoxSet query");
return true;
@@ -844,7 +845,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsPlayed.HasValue)
{
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name))
+ if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series)))
{
Logger.LogDebug("Query requires post-filtering due to IsPlayed");
return true;
@@ -1016,7 +1017,8 @@ namespace MediaBrowser.Controller.Entities
return items;
}
- private static bool CollapseBoxSetItems(InternalItemsQuery query,
+ private static bool CollapseBoxSetItems(
+ InternalItemsQuery query,
BaseItem queryParent,
User user,
IServerConfigurationManager configurationManager)
@@ -1645,7 +1647,8 @@ namespace MediaBrowser.Controller.Entities
/// <param name="datePlayed">The date played.</param>
/// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
/// <returns>Task.</returns>
- public override void MarkPlayed(User user,
+ public override void MarkPlayed(
+ User user,
DateTime? datePlayed,
bool resetPosition)
{
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index db6c85caf..74a170204 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -59,7 +59,13 @@ namespace MediaBrowser.Controller.Entities
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
- query.ExcludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+ query.ExcludeItemTypes = new[]
+ {
+ nameof(MusicVideo),
+ nameof(Entities.Audio.Audio),
+ nameof(MusicAlbum),
+ nameof(MusicArtist)
+ };
return LibraryManager.GetItemList(query);
}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index 4e09ee573..5b96a5af6 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Entities
{
@@ -23,6 +24,10 @@ namespace MediaBrowser.Controller.Entities
public string NameContains { get; set; }
+ public User User { get; set; }
+
+ public bool? IsFavorite { get; set; }
+
public InternalPeopleQuery()
{
PersonTypes = Array.Empty<string>();
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 75a746bfb..e8afa9a49 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -151,7 +151,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
}
query.IsVirtualItem = false;
@@ -207,7 +207,7 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
- query.IncludeItemTypes = new[] { typeof(Season).Name };
+ query.IncludeItemTypes = new[] { nameof(Season) };
query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
if (user != null && !user.DisplayMissingEpisodes)
@@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) };
}
query.IsVirtualItem = false;
@@ -253,7 +253,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name },
+ IncludeItemTypes = new[] { nameof(Episode), nameof(Season) },
OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
};
@@ -364,7 +364,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
};
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 068a76769..a262fee15 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -142,7 +142,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
}
return parent.QueryRecursive(query);
@@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -178,7 +178,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -189,7 +189,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
return _libraryManager.GetItemsResult(query);
}
@@ -200,7 +200,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+ query.IncludeItemTypes = new[] { nameof(BoxSet) };
query.SetUser(user);
query.Recursive = true;
@@ -223,7 +223,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -236,7 +236,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -255,7 +255,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Movie).Name },
+ IncludeItemTypes = new[] { nameof(Movie) },
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -286,7 +286,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
query.IsVirtualItem = false;
return ConvertToResult(_libraryManager.GetItemList(query));
@@ -343,7 +343,8 @@ namespace MediaBrowser.Controller.Entities
{
var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty });
- var result = _tvSeriesManager.GetNextUp(new NextUpQuery
+ var result = _tvSeriesManager.GetNextUp(
+ new NextUpQuery
{
Limit = query.Limit,
StartIndex = query.StartIndex,
@@ -361,7 +362,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -372,7 +373,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -381,7 +382,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Series).Name },
+ IncludeItemTypes = new[] { nameof(Series) },
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -412,7 +413,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -443,7 +444,8 @@ namespace MediaBrowser.Controller.Entities
return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager);
}
- public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items,
+ public static QueryResult<BaseItem> PostFilterAndSort(
+ IEnumerable<BaseItem> items,
BaseItem queryParent,
int? totalRecordLimit,
InternalItemsQuery query,
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
index b35f83096..6658269bd 100644
--- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -12,6 +12,9 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the display preferences for the user and client.
/// </summary>
+ /// <remarks>
+ /// This will create the display preferences if it does not exist, but it will not save automatically.
+ /// </remarks>
/// <param name="userId">The user's id.</param>
/// <param name="client">The client string.</param>
/// <returns>The associated display preferences.</returns>
@@ -20,6 +23,9 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the default item display preferences for the user and client.
/// </summary>
+ /// <remarks>
+ /// This will create the item display preferences if it does not exist, but it will not save automatically.
+ /// </remarks>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index cfad17fb7..ffbb147b0 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -6,8 +6,8 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.System;
-using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller
{
@@ -56,10 +56,11 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the system info.
/// </summary>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
/// <returns>SystemInfo.</returns>
- Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
+ Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken = default);
- Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
+ Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken = default);
/// <summary>
/// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
@@ -67,7 +68,7 @@ namespace MediaBrowser.Controller
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
/// <returns>A list containing all the local IP addresses of the server.</returns>
- Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
+ Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken = default);
/// <summary>
/// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
@@ -75,7 +76,7 @@ namespace MediaBrowser.Controller
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
/// <returns>The server URL.</returns>
- Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
+ Task<string> GetLocalApiUrl(CancellationToken cancellationToken = default);
/// <summary>
/// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
@@ -119,5 +120,13 @@ namespace MediaBrowser.Controller
string ExpandVirtualPath(string path);
string ReverseVirtualPath(string path);
+
+ /// <summary>
+ /// Gets the list of local plugins.
+ /// </summary>
+ /// <param name="path">Plugin base directory.</param>
+ /// <param name="cleanup">Cleanup old plugins.</param>
+ /// <returns>Enumerable of local plugins.</returns>
+ IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true);
}
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 332730bcc..c7c79df76 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -566,8 +566,11 @@ namespace MediaBrowser.Controller.Library
int GetCount(InternalItemsQuery query);
- void AddExternalSubtitleStreams(List<MediaStream> streams,
+ void AddExternalSubtitleStreams(
+ List<MediaStream> streams,
string videoPath,
string[] files);
+
+ BaseItem GetParentItem(string parentId, Guid? userId);
}
}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 22bf9488f..21c6ef2af 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -115,5 +115,7 @@ namespace MediaBrowser.Controller.Library
public interface IDirectStreamProvider
{
Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
+
+ string GetFilePath();
}
}
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 6a4f5cf67..8fd3b8c34 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -158,7 +158,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <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);
+ /// <returns>A task representing the update.</returns>
+ Task UpdateConfigurationAsync(Guid userId, UserConfiguration config);
/// <summary>
/// This method updates the user's policy.
@@ -167,12 +168,14 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <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);
+ /// <returns>A task representing the update.</returns>
+ Task UpdatePolicyAsync(Guid userId, UserPolicy policy);
/// <summary>
/// Clears the user's profile image.
/// </summary>
/// <param name="user">The user.</param>
- void ClearProfileImage(User user);
+ /// <returns>A task representing the clearing of the profile image.</returns>
+ Task ClearProfileImageAsync(User user);
}
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 243b8cd02..b2bce02a2 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,9 +14,9 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.8" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.9" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.11.1" />
</ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index c5529ad5b..5846a603a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -70,15 +70,15 @@ namespace MediaBrowser.Controller.MediaEncoding
var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
- {"qsv", hwEncoder + "_qsv"},
- {hwEncoder + "_qsv", hwEncoder + "_qsv"},
- {"nvenc", hwEncoder + "_nvenc"},
- {"amf", hwEncoder + "_amf"},
- {"omx", hwEncoder + "_omx"},
- {hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m"},
- {"mediacodec", hwEncoder + "_mediacodec"},
- {"vaapi", hwEncoder + "_vaapi"},
- {"videotoolbox", hwEncoder + "_videotoolbox"}
+ { "qsv", hwEncoder + "_qsv" },
+ { hwEncoder + "_qsv", hwEncoder + "_qsv" },
+ { "nvenc", hwEncoder + "_nvenc" },
+ { "amf", hwEncoder + "_amf" },
+ { "omx", hwEncoder + "_omx" },
+ { hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m" },
+ { "mediacodec", hwEncoder + "_mediacodec" },
+ { "vaapi", hwEncoder + "_vaapi" },
+ { "videotoolbox", hwEncoder + "_videotoolbox" }
};
if (!string.IsNullOrEmpty(hwType)
@@ -451,11 +451,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var arg = new StringBuilder();
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+ var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+ var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
- var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+ var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
@@ -517,11 +519,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (state.IsVideoRequest
- && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ && (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder)
+ || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder))
{
var isColorDepth10 = IsColorDepth10(state);
- if (isNvencHevcDecoder && isColorDepth10
+ if (isColorDepth10
&& _mediaEncoder.SupportsHwaccel("opencl")
&& encodingOptions.EnableTonemapping
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
@@ -880,6 +883,19 @@ namespace MediaBrowser.Controller.MediaEncoding
param += "-quality speed";
break;
}
+
+ var videoStream = state.VideoStream;
+ var isColorDepth10 = IsColorDepth10(state);
+
+ if (isColorDepth10
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && encodingOptions.EnableTonemapping
+ && !string.IsNullOrEmpty(videoStream.VideoRange)
+ && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ // Enhance workload when tone mapping with AMF on some APUs
+ param += " -preanalysis true";
+ }
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{
@@ -1023,19 +1039,19 @@ namespace MediaBrowser.Controller.MediaEncoding
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
param = "-pix_fmt yuv420p " + param;
}
- if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase))
{
- var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
var videoStream = state.VideoStream;
var isColorDepth10 = IsColorDepth10(state);
- if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1
- && isColorDepth10
+ if (isColorDepth10
&& _mediaEncoder.SupportsHwaccel("opencl")
&& encodingOptions.EnableTonemapping
&& !string.IsNullOrEmpty(videoStream.VideoRange)
@@ -1364,24 +1380,40 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
{
+ if (audioStream == null)
+ {
+ return null;
+ }
+
if (request.AudioBitRate.HasValue)
{
// Don't encode any higher than this
return Math.Min(384000, request.AudioBitRate.Value);
}
- return null;
+ // Empty bitrate area is not allow on iOS
+ // Default audio bitrate to 128K if it is not being requested
+ // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
+ return 128000;
}
public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
{
+ if (audioStream == null)
+ {
+ return null;
+ }
+
if (audioBitRate.HasValue)
{
// Don't encode any higher than this
return Math.Min(384000, audioBitRate.Value);
}
- return null;
+ // Empty bitrate area is not allow on iOS
+ // Default audio bitrate to 128K if it is not being requested
+ // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
+ return 128000;
}
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
@@ -1651,47 +1683,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputSizeParam = ReadOnlySpan<char>.Empty;
var request = state.BaseRequest;
- outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
-
- // All possible beginning of video filters
- // Don't break the order
- string[] beginOfOutputSizeParam = new[]
- {
- // for tonemap_opencl
- "hwupload,tonemap_opencl",
-
- // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
- "hwupload=extra_hw_frames",
-
- // vpp_qsv
- "vpp",
-
- // hwdownload,format=p010le (hardware decode + software encode for vaapi)
- "hwdownload",
-
- // format=nv12|vaapi,hwupload,scale_vaapi
- "format",
-
- // bwdif,scale=expr
- "bwdif",
-
- // yadif,scale=expr
- "yadif",
-
- // scale=expr
- "scale"
- };
-
- var index = -1;
- foreach (var param in beginOfOutputSizeParam)
- {
- index = outputSizeParam.IndexOf(param, StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- break;
- }
- }
+ outputSizeParam = GetOutputSizeParamInternal(state, options, outputVideoCodec);
var videoSizeParam = string.Empty;
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
@@ -1822,7 +1814,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight));
}
- public List<string> GetScalingFilters(EncodingJobInfo state,
+ public List<string> GetScalingFilters(
+ EncodingJobInfo state,
int? videoWidth,
int? videoHeight,
Video3DFormat? threedFormat,
@@ -2082,10 +2075,19 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam);
}
+ public string GetOutputSizeParam(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string outputVideoCodec)
+ {
+ string filters = GetOutputSizeParamInternal(state, options, outputVideoCodec);
+ return string.IsNullOrEmpty(filters) ? string.Empty : " -vf \"" + filters + "\"";
+ }
+
/// <summary>
/// If we're going to put a fixed size on the command line, this will calculate it.
/// </summary>
- public string GetOutputSizeParam(
+ public string GetOutputSizeParamInternal(
EncodingJobInfo state,
EncodingOptions options,
string outputVideoCodec)
@@ -2101,6 +2103,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream?.Height;
var threeDFormat = state.MediaSource.Video3DFormat;
+ var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+ var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
@@ -2116,47 +2120,77 @@ namespace MediaBrowser.Controller.MediaEncoding
// If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30;
- // Currently only with the use of NVENC decoder can we get a decent performance.
- // Currently only the HEVC/H265 format is supported.
- // NVIDIA Pascal and Turing or higher are recommended.
- if (isNvdecHevcDecoder && isColorDepth10
- && _mediaEncoder.SupportsHwaccel("opencl")
- && options.EnableTonemapping
- && !string.IsNullOrEmpty(videoStream.VideoRange)
- && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
- {
- var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
+ var isScalingInAdvance = false;
+ var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
- if (options.TonemappingParam != 0)
+ if ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder)
+ || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder))
+ {
+ // Currently only with the use of NVENC decoder can we get a decent performance.
+ // Currently only the HEVC/H265 format is supported with NVDEC decoder.
+ // NVIDIA Pascal and Turing or higher are recommended.
+ // AMD Polaris and Vega or higher are recommended.
+ if (isColorDepth10
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && options.EnableTonemapping
+ && !string.IsNullOrEmpty(videoStream.VideoRange)
+ && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
{
- parameters += ":param={4}";
- }
+ var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
- if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
- {
- parameters += ":range={5}";
- }
+ if (options.TonemappingParam != 0)
+ {
+ parameters += ":param={4}";
+ }
- // Upload the HDR10 or HLG data to the OpenCL device,
- // use tonemap_opencl filter for tone mapping,
- // and then download the SDR data to memory.
- filters.Add("hwupload");
- filters.Add(
- string.Format(
- CultureInfo.InvariantCulture,
- parameters,
- options.TonemappingAlgorithm,
- options.TonemappingDesat,
- options.TonemappingThreshold,
- options.TonemappingPeak,
- options.TonemappingParam,
- options.TonemappingRange));
- filters.Add("hwdownload");
-
- if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)
- || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
- {
- filters.Add("format=nv12");
+ if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+ {
+ parameters += ":range={5}";
+ }
+
+ if (isSwDecoder || isD3d11vaDecoder)
+ {
+ isScalingInAdvance = true;
+ // Add zscale filter before tone mapping filter for performance.
+ var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
+ if (width.HasValue && height.HasValue)
+ {
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "zscale=s={0}x{1}",
+ width.Value,
+ height.Value));
+ }
+
+ // Convert to hardware pixel format p010 when using SW decoder.
+ filters.Add("format=p010");
+ }
+
+ // Upload the HDR10 or HLG data to the OpenCL device,
+ // use tonemap_opencl filter for tone mapping,
+ // and then download the SDR data to memory.
+ filters.Add("hwupload");
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ parameters,
+ options.TonemappingAlgorithm,
+ options.TonemappingDesat,
+ options.TonemappingThreshold,
+ options.TonemappingPeak,
+ options.TonemappingParam,
+ options.TonemappingRange));
+ filters.Add("hwdownload");
+
+ if (isLibX264Encoder
+ || hasGraphicalSubs
+ || (isNvdecHevcDecoder && isDeinterlaceHevc)
+ || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
+ {
+ filters.Add("format=nv12");
+ }
}
}
@@ -2201,7 +2235,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add hardware deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
+ if (isDeinterlaceH264)
{
if (isVaapiH264Encoder)
{
@@ -2214,10 +2248,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add software deinterlace filter before scaling filter
- if ((state.DeInterlace("h264", true)
- || state.DeInterlace("avc", true)
- || state.DeInterlace("h265", true)
- || state.DeInterlace("hevc", true))
+ if ((isDeinterlaceH264 || isDeinterlaceHevc)
&& !isVaapiH264Encoder
&& !isQsvH264Encoder
&& !isNvdecH264Decoder)
@@ -2241,7 +2272,21 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
- filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
+ if (!isScalingInAdvance)
+ {
+ filters.AddRange(
+ GetScalingFilters(
+ state,
+ inputWidth,
+ inputHeight,
+ threeDFormat,
+ videoDecoder,
+ outputVideoCodec,
+ request.Width,
+ request.Height,
+ request.MaxWidth,
+ request.MaxHeight));
+ }
// Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
if (isVaapiH264Encoder)
@@ -2274,7 +2319,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
output += string.Format(
CultureInfo.InvariantCulture,
- " -vf \"{0}\"",
+ "{0}",
string.Join(",", filters));
}
@@ -2630,9 +2675,10 @@ namespace MediaBrowser.Controller.MediaEncoding
state.MediaSource = mediaSource;
var request = state.BaseRequest;
- if (!string.IsNullOrWhiteSpace(request.AudioCodec))
+ var supportedAudioCodecs = state.SupportedAudioCodecs;
+ if (request != null && supportedAudioCodecs != null && supportedAudioCodecs.Length > 0)
{
- var supportedAudioCodecsList = request.AudioCodec.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
+ var supportedAudioCodecsList = supportedAudioCodecs.ToList();
ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
@@ -3039,7 +3085,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- var whichCodec = videoStream.Codec.ToLowerInvariant();
+ var whichCodec = videoStream.Codec?.ToLowerInvariant();
switch (whichCodec)
{
case "avc":
@@ -3067,21 +3113,31 @@ namespace MediaBrowser.Controller.MediaEncoding
var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
- if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
+ if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
{
- if (isLinux)
+ // Currently there is no AMF decoder on Linux, only have h264 encoder.
+ if (isDxvaSupported && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- return "-hwaccel vaapi";
- }
+ if (isWindows && isWindows8orLater)
+ {
+ return "-hwaccel d3d11va";
+ }
- if (isWindows && isWindows8orLater)
- {
- return "-hwaccel d3d11va";
+ if (isWindows && !isWindows8orLater)
+ {
+ return "-hwaccel dxva2";
+ }
}
+ }
- if (isWindows && !isWindows8orLater)
+ if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ if (IsVaapiSupported(state) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- return "-hwaccel dxva2";
+ if (isLinux)
+ {
+ return "-hwaccel vaapi";
+ }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index c7ec878d2..6e9362cd1 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -287,6 +287,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return BaseRequest.AudioChannels;
}
+ if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
+ {
+ return BaseRequest.TranscodingMaxAudioChannels;
+ }
+
if (!string.IsNullOrEmpty(codec))
{
var value = BaseRequest.GetOption(codec, "audiochannels");
@@ -342,7 +347,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
- var newSize = DrawingUtils.Resize(size,
+ var newSize = DrawingUtils.Resize(
+ size,
BaseRequest.Width ?? 0,
BaseRequest.Height ?? 0,
BaseRequest.MaxWidth ?? 0,
@@ -368,7 +374,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
- var newSize = DrawingUtils.Resize(size,
+ var newSize = DrawingUtils.Resize(
+ size,
BaseRequest.Width ?? 0,
BaseRequest.Height ?? 0,
BaseRequest.MaxWidth ?? 0,
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 17d6dc5d2..f6bc1f4de 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -68,7 +68,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Extracts the video images on interval.
/// </summary>
- Task ExtractVideoImagesOnInterval(string[] inputFiles,
+ Task ExtractVideoImagesOnInterval(
+ string[] inputFiles,
string container,
MediaStream videoStream,
MediaProtocol protocol,
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
index 735c46ef8..0194c596f 100644
--- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -1,10 +1,11 @@
-#pragma warning disable CS1591
-
using System;
using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// The request authorization info.
+ /// </summary>
public class AuthorizationInfo
{
/// <summary>
@@ -43,6 +44,19 @@ namespace MediaBrowser.Controller.Net
/// <value>The token.</value>
public string Token { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the authorization is from an api key.
+ /// </summary>
+ public bool IsApiKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user making the request.
+ /// </summary>
public User User { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the token is authenticated.
+ /// </summary>
+ public bool IsAuthenticated { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 916dea58b..28227603b 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -8,6 +8,7 @@ using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Net
@@ -28,10 +29,22 @@ namespace MediaBrowser.Controller.Net
new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
/// <summary>
- /// Gets the name.
+ /// Gets the type used for the messages sent to the client.
/// </summary>
- /// <value>The name.</value>
- protected abstract string Name { get; }
+ /// <value>The type.</value>
+ protected abstract SessionMessageType Type { get; }
+
+ /// <summary>
+ /// Gets the message type received from the client to start sending messages.
+ /// </summary>
+ /// <value>The type.</value>
+ protected abstract SessionMessageType StartType { get; }
+
+ /// <summary>
+ /// Gets the message type received from the client to stop sending messages.
+ /// </summary>
+ /// <value>The type.</value>
+ protected abstract SessionMessageType StopType { get; }
/// <summary>
/// Gets the data to send.
@@ -66,12 +79,12 @@ namespace MediaBrowser.Controller.Net
throw new ArgumentNullException(nameof(message));
}
- if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase))
+ if (message.MessageType == StartType)
{
Start(message);
}
- if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase))
+ if (message.MessageType == StopType)
{
Stop(message);
}
@@ -159,7 +172,7 @@ namespace MediaBrowser.Controller.Net
new WebSocketMessage<TReturnDataType>
{
MessageId = Guid.NewGuid(),
- MessageType = Name,
+ MessageType = Type,
Data = data
},
cancellationToken).ConfigureAwait(false);
@@ -176,7 +189,7 @@ namespace MediaBrowser.Controller.Net
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error sending web socket message {Name}", Name);
+ Logger.LogError(ex, "Error sending web socket message {Name}", Type);
DisposeConnection(tuple);
}
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs
index e9f00ae88..ce74173e7 100644
--- a/MediaBrowser.Controller/Net/IWebSocketManager.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs
@@ -17,12 +17,6 @@ namespace MediaBrowser.Controller.Net
event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
/// <summary>
- /// Inits this instance.
- /// </summary>
- /// <param name="listeners">The websocket listeners.</param>
- void Init(IEnumerable<IWebSocketListener> listeners);
-
- /// <summary>
/// The HTTP request handler.
/// </summary>
/// <param name="context">The current HTTP context.</param>
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 216dd2709..e8b7be7e2 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -160,7 +160,7 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = new[] { musicGenre.Id },
OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
@@ -172,7 +172,7 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
ArtistIds = new[] { musicArtist.Id },
OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
DtoOptions = options
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index eb7fb793a..75286eadc 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -29,7 +29,8 @@ namespace MediaBrowser.Controller.Resolvers
public interface IMultiItemResolver
{
- MultiItemResolverResult ResolveMultiple(Folder parent,
+ MultiItemResolverResult ResolveMultiple(
+ Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService);
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index 22d6e2a04..bc4ccd44c 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -3,6 +3,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Session
{
@@ -23,6 +24,6 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message.
/// </summary>
- Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
+ Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 228b2331d..04c3004ee 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -188,16 +188,16 @@ namespace MediaBrowser.Controller.Session
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user sessions.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>Task.</returns>
- Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken);
- Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken);
+ Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user device sessions.
@@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken);
/// <summary>
/// Sends the restart required message.
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 55e44c19d..ce58a60b9 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -230,8 +230,8 @@ namespace MediaBrowser.Controller.Session
/// Gets or sets the supported commands.
/// </summary>
/// <value>The supported commands.</value>
- public string[] SupportedCommands
- => Capabilities == null ? Array.Empty<string>() : Capabilities.SupportedCommands;
+ public GeneralCommandType[] SupportedCommands
+ => Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
{
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index f43d523a6..feb26bc10 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -53,6 +54,14 @@ namespace MediaBrowser.Controller.Subtitles
Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken);
/// <summary>
+ /// Upload new subtitle.
+ /// </summary>
+ /// <param name="video">The video the subtitle belongs to.</param>
+ /// <param name="response">The subtitle response.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task UploadSubtitle(Video video, SubtitleResponse response);
+
+ /// <summary>
/// Gets the remote subtitles.
/// </summary>
/// <param name="id">The identifier.</param>
diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
index e742df517..a1cada25c 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
@@ -14,12 +14,12 @@ namespace MediaBrowser.Controller.SyncPlay
public class GroupInfo
{
/// <summary>
- /// Gets the default ping value used for sessions.
+ /// The default ping value used for sessions.
/// </summary>
- public long DefaultPing { get; } = 500;
+ public const long DefaultPing = 500;
/// <summary>
- /// Gets or sets the group identifier.
+ /// Gets the group identifier.
/// </summary>
/// <value>The group identifier.</value>
public Guid GroupId { get; } = Guid.NewGuid();
@@ -58,7 +58,8 @@ namespace MediaBrowser.Controller.SyncPlay
/// <summary>
/// Checks if a session is in this group.
/// </summary>
- /// <value><c>true</c> if the session is in this group; <c>false</c> otherwise.</value>
+ /// <param name="sessionId">The session id to check.</param>
+ /// <returns><c>true</c> if the session is in this group; <c>false</c> otherwise.</returns>
public bool ContainsSession(string sessionId)
{
return Participants.ContainsKey(sessionId);
@@ -70,16 +71,14 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public void AddSession(SessionInfo session)
{
- if (ContainsSession(session.Id))
- {
- return;
- }
-
- var member = new GroupMember();
- member.Session = session;
- member.Ping = DefaultPing;
- member.IsBuffering = false;
- Participants[session.Id] = member;
+ Participants.TryAdd(
+ session.Id,
+ new GroupMember
+ {
+ Session = session,
+ Ping = DefaultPing,
+ IsBuffering = false
+ });
}
/// <summary>
@@ -88,12 +87,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public void RemoveSession(SessionInfo session)
{
- if (!ContainsSession(session.Id))
- {
- return;
- }
-
- Participants.Remove(session.Id, out _);
+ Participants.Remove(session.Id);
}
/// <summary>
@@ -103,18 +97,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="ping">The ping.</param>
public void UpdatePing(SessionInfo session, long ping)
{
- if (!ContainsSession(session.Id))
+ if (Participants.TryGetValue(session.Id, out GroupMember value))
{
- return;
+ value.Ping = ping;
}
-
- Participants[session.Id].Ping = ping;
}
/// <summary>
/// Gets the highest ping in the group.
/// </summary>
- /// <value name="session">The highest ping in the group.</value>
+ /// <returns>The highest ping in the group.</returns>
public long GetHighestPing()
{
long max = long.MinValue;
@@ -133,18 +125,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="isBuffering">The state.</param>
public void SetBuffering(SessionInfo session, bool isBuffering)
{
- if (!ContainsSession(session.Id))
+ if (Participants.TryGetValue(session.Id, out GroupMember value))
{
- return;
+ value.IsBuffering = isBuffering;
}
-
- Participants[session.Id].IsBuffering = isBuffering;
}
/// <summary>
/// Gets the group buffering state.
/// </summary>
- /// <value><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</value>
+ /// <returns><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</returns>
public bool IsBuffering()
{
foreach (var session in Participants.Values)
@@ -161,7 +151,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <summary>
/// Checks if the group is empty.
/// </summary>
- /// <value><c>true</c> if the group is empty; <c>false</c> otherwise.</value>
+ /// <returns><c>true</c> if the group is empty; <c>false</c> otherwise.</returns>
public bool IsEmpty()
{
return Participants.Count == 0;
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 22537a4d9..cdeefbbbd 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -666,6 +666,16 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
+ // Interlaced video streams in Matroska containers return the field rate instead of the frame rate
+ // as both the average and real frame rate, so we half the returned frame rates to get the correct values
+ //
+ // https://gitlab.com/mbunkus/mkvtoolnix/-/wikis/Wrong-frame-rate-displayed
+ if (stream.IsInterlaced && formatInfo.FormatName.Contains("matroska", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.AverageFrameRate /= 2;
+ stream.RealFrameRate /= 2;
+ }
+
if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
{
diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs
index d5344494e..28073fb8d 100644
--- a/MediaBrowser.Model/Activity/IActivityManager.cs
+++ b/MediaBrowser.Model/Activity/IActivityManager.cs
@@ -1,10 +1,10 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Model.Activity
@@ -15,11 +15,13 @@ namespace MediaBrowser.Model.Activity
Task CreateAsync(ActivityLog entry);
- QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit);
+ Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query);
- QueryResult<ActivityLogEntry> GetPagedResult(
- Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
- int? startIndex,
- int? limit);
+ /// <summary>
+ /// Remove all activity logs before the specified date.
+ /// </summary>
+ /// <param name="startDate">Activity log start date.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task CleanAsync(DateTime startDate);
}
}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 2cd637c5b..c34825667 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Configuration
public string TranscodingTempPath { get; set; }
+ public string FallbackFontPath { get; set; }
+
+ public bool EnableFallbackFont { get; set; }
+
public double DownMixAudioBoost { get; set; }
public int MaxMuxingQueueSize { get; set; }
@@ -69,6 +73,7 @@ namespace MediaBrowser.Model.Configuration
public EncodingOptions()
{
+ EnableFallbackFont = false;
DownMixAudioBoost = 2;
MaxMuxingQueueSize = 2048;
EnableThrottling = false;
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 890469d36..54ef49ea6 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -25,8 +25,6 @@ namespace MediaBrowser.Model.Configuration
public bool EnableInternetProviders { get; set; }
- public bool ImportMissingEpisodes { get; set; }
-
public bool EnableAutomaticSeriesGrouping { get; set; }
public bool EnableEmbeddedTitles { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 14bfcbf9e..4d24e45b0 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -271,6 +271,10 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
public string[] KnownProxies { get; set; }
+ /// Gets or sets the number of days we should retain activity logs.
+ /// </summary>
+ public int? ActivityLogRetentionDays { get; set; }
+
/// <summary>
/// Gets or sets the how the library scan fans out.
/// </summary>
@@ -391,8 +395,9 @@ namespace MediaBrowser.Model.Configuration
SlowResponseThresholdMs = 500;
CorsHosts = new[] { "*" };
KnownProxies = Array.Empty<string>();
- LibraryMetadataRefreshConcurrency = 0;
+ ActivityLogRetentionDays = 30;
LibraryScanFanoutConcurrency = 0;
+ LibraryMetadataRefreshConcurrency = 0;
}
}
diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs
index 67e4ffe03..bbb8bf426 100644
--- a/MediaBrowser.Model/Dlna/AudioOptions.cs
+++ b/MediaBrowser.Model/Dlna/AudioOptions.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// The application's configured quality setting.
/// </summary>
- public long? MaxBitrate { get; set; }
+ public int? MaxBitrate { get; set; }
/// <summary>
/// Gets or sets the context.
@@ -67,7 +67,7 @@ namespace MediaBrowser.Model.Dlna
/// Gets the maximum bitrate.
/// </summary>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
- public long? GetMaxBitrate(bool isAudio)
+ public int? GetMaxBitrate(bool isAudio)
{
if (MaxBitrate.HasValue)
{
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 93e60753a..8b73ecbd4 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -38,7 +38,8 @@ namespace MediaBrowser.Model.Dlna
";DLNA.ORG_FLAGS={0}",
DlnaMaps.FlagsToString(flagValue));
- ResponseProfile mediaProfile = _profile.GetImageMediaProfile(container,
+ ResponseProfile mediaProfile = _profile.GetImageMediaProfile(
+ container,
width,
height);
@@ -160,7 +161,8 @@ namespace MediaBrowser.Model.Dlna
string dlnaflags = string.Format(CultureInfo.InvariantCulture, ";DLNA.ORG_FLAGS={0}",
DlnaMaps.FlagsToString(flagValue));
- ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(container,
+ ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(
+ container,
audioCodec,
videoCodec,
width,
@@ -221,7 +223,8 @@ namespace MediaBrowser.Model.Dlna
private static string GetImageOrgPnValue(string container, int? width, int? height)
{
MediaFormatProfile? format = new MediaFormatProfileResolver()
- .ResolveImageFormat(container,
+ .ResolveImageFormat(
+ container,
width,
height);
@@ -231,7 +234,8 @@ namespace MediaBrowser.Model.Dlna
private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels)
{
MediaFormatProfile? format = new MediaFormatProfileResolver()
- .ResolveAudioFormat(container,
+ .ResolveAudioFormat(
+ container,
audioBitrate,
audioSampleRate,
audioChannels);
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 7e921b1fd..e842efead 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -62,9 +62,9 @@ namespace MediaBrowser.Model.Dlna
public int? MaxIconHeight { get; set; }
- public long? MaxStreamingBitrate { get; set; }
+ public int? MaxStreamingBitrate { get; set; }
- public long? MaxStaticBitrate { get; set; }
+ public int? MaxStaticBitrate { get; set; }
public int? MusicStreamingTranscodingBitrate { get; set; }
@@ -277,7 +277,8 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public ResponseProfile GetVideoMediaProfile(string container,
+ public ResponseProfile GetVideoMediaProfile(
+ string container,
string audioCodec,
string videoCodec,
int? width,
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index d9e7e4fbb..4959a9b92 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -455,9 +455,11 @@ namespace MediaBrowser.Model.Dlna
if (directPlayProfile == null)
{
- _logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}",
+ _logger.LogInformation(
+ "Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
options.Profile.Name ?? "Unknown Profile",
- item.Path ?? "Unknown path");
+ item.Path ?? "Unknown path",
+ audioStream.Codec ?? "Unknown codec");
return (Enumerable.Empty<PlayMethod>(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
@@ -678,7 +680,8 @@ namespace MediaBrowser.Model.Dlna
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1);
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1);
- _logger.LogInformation("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
+ _logger.LogInformation(
+ "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path",
isEligibleForDirectPlay,
@@ -972,9 +975,11 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == null)
{
- _logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}",
+ _logger.LogInformation(
+ "Profile: {0}, No video direct play profiles found for {1} with codec {2}",
profile.Name ?? "Unknown Profile",
- mediaSource.Path ?? "Unknown path");
+ mediaSource.Path ?? "Unknown path",
+ videoStream.Codec ?? "Unknown codec");
return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles));
}
@@ -1135,7 +1140,8 @@ namespace MediaBrowser.Model.Dlna
private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
{
- _logger.LogInformation("Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
+ _logger.LogInformation(
+ "Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
type,
profile.Name ?? "Unknown Profile",
condition.Property,
@@ -1340,7 +1346,8 @@ namespace MediaBrowser.Model.Dlna
if (itemBitrate > requestedMaxBitrate)
{
- _logger.LogInformation("Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
+ _logger.LogInformation(
+ "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
playMethod, itemBitrate, requestedMaxBitrate);
return false;
}
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
new file mode 100644
index 000000000..712fa381e
--- /dev/null
+++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Model.Extensions
+{
+ /// <summary>
+ /// Extension methods for <see cref="IEnumerable{T}"/>.
+ /// </summary>
+ public static class EnumerableExtensions
+ {
+ /// <summary>
+ /// Orders <see cref="RemoteImageInfo"/> by requested language in descending order, prioritizing "en" over other non-matches.
+ /// </summary>
+ /// <param name="remoteImageInfos">The remote image infos.</param>
+ /// <param name="requestedLanguage">The requested language for the images.</param>
+ /// <returns>The ordered remote image infos.</returns>
+ public static IEnumerable<RemoteImageInfo> OrderByLanguageDescending(this IEnumerable<RemoteImageInfo> remoteImageInfos, string requestedLanguage)
+ {
+ var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase);
+
+ return remoteImageInfos.OrderByDescending(i =>
+ {
+ if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 3;
+ }
+
+ if (!isRequestedLanguageEn && string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+
+ if (string.IsNullOrEmpty(i.Language))
+ {
+ return isRequestedLanguageEn ? 3 : 2;
+ }
+
+ return 0;
+ })
+ .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => i.VoteCount ?? 0);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 264681090..253ee7e79 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -34,7 +34,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.9" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="5.0.0-preview.8.20407.11" />
</ItemGroup>
diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
index 83bda5d56..a8ea405e2 100644
--- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
+++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
@@ -37,7 +37,7 @@ namespace MediaBrowser.Model.MediaInfo
public string PlaySessionId { get; set; }
- public long? MaxStreamingBitrate { get; set; }
+ public int? MaxStreamingBitrate { get; set; }
public long? StartTimeTicks { get; set; }
diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs
index 660eebeda..bffbbe612 100644
--- a/MediaBrowser.Model/Net/WebSocketMessage.cs
+++ b/MediaBrowser.Model/Net/WebSocketMessage.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using MediaBrowser.Model.Session;
namespace MediaBrowser.Model.Net
{
@@ -15,7 +16,7 @@ namespace MediaBrowser.Model.Net
/// Gets or sets the type of the message.
/// </summary>
/// <value>The type of the message.</value>
- public string MessageType { get; set; }
+ public SessionMessageType MessageType { get; set; }
public Guid MessageId { get; set; }
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index d3878ca30..a85e6ff2a 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Session
{
public string[] PlayableMediaTypes { get; set; }
- public string[] SupportedCommands { get; set; }
+ public GeneralCommandType[] SupportedCommands { get; set; }
public bool SupportsMediaControl { get; set; }
@@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Session
public ClientCapabilities()
{
PlayableMediaTypes = Array.Empty<string>();
- SupportedCommands = Array.Empty<string>();
+ SupportedCommands = Array.Empty<GeneralCommandType>();
SupportsPersistentIdentifier = true;
}
}
diff --git a/MediaBrowser.Model/Session/GeneralCommandType.cs b/MediaBrowser.Model/Session/GeneralCommandType.cs
index 5a9042d5f..c58fa9a6b 100644
--- a/MediaBrowser.Model/Session/GeneralCommandType.cs
+++ b/MediaBrowser.Model/Session/GeneralCommandType.cs
@@ -43,6 +43,11 @@ namespace MediaBrowser.Model.Session
Guide = 32,
ToggleStats = 33,
PlayMediaSource = 34,
- PlayTrailers = 35
+ PlayTrailers = 35,
+ SetShuffleQueue = 36,
+ PlayState = 37,
+ PlayNext = 38,
+ ToggleOsdMenu = 39,
+ Play = 40
}
}
diff --git a/MediaBrowser.Model/Session/SessionMessageType.cs b/MediaBrowser.Model/Session/SessionMessageType.cs
new file mode 100644
index 000000000..23c41026d
--- /dev/null
+++ b/MediaBrowser.Model/Session/SessionMessageType.cs
@@ -0,0 +1,50 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// The different kinds of messages that are used in the WebSocket api.
+ /// </summary>
+ public enum SessionMessageType
+ {
+ // Server -> Client
+ ForceKeepAlive,
+ GeneralCommand,
+ UserDataChanged,
+ Sessions,
+ Play,
+ SyncPlayCommand,
+ SyncPlayGroupUpdate,
+ PlayState,
+ RestartRequired,
+ ServerShuttingDown,
+ ServerRestarting,
+ LibraryChanged,
+ UserDeleted,
+ UserUpdated,
+ SeriesTimerCreated,
+ TimerCreated,
+ SeriesTimerCancelled,
+ TimerCancelled,
+ RefreshProgress,
+ ScheduledTaskEnded,
+ PackageInstallationCancelled,
+ PackageInstallationFailed,
+ PackageInstallationCompleted,
+ PackageInstalling,
+ PackageUninstalled,
+ ActivityLogEntry,
+ ScheduledTasksInfo,
+
+ // Client -> Server
+ ActivityLogEntryStart,
+ ActivityLogEntryStop,
+ SessionsStart,
+ SessionsStop,
+ ScheduledTasksInfoStart,
+ ScheduledTasksInfoStop,
+
+ // Shared
+ KeepAlive,
+ }
+}
diff --git a/MediaBrowser.Model/Subtitles/FontFile.cs b/MediaBrowser.Model/Subtitles/FontFile.cs
new file mode 100644
index 000000000..115c49295
--- /dev/null
+++ b/MediaBrowser.Model/Subtitles/FontFile.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Model.Subtitles
+{
+ /// <summary>
+ /// Class FontFile.
+ /// </summary>
+ public class FontFile
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size.
+ /// </summary>
+ /// <value>The size.</value>
+ public long Size { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <value>The date modified.</value>
+ public DateTime DateModified { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs
index d2f7556a5..53030843a 100644
--- a/MediaBrowser.Model/System/PublicSystemInfo.cs
+++ b/MediaBrowser.Model/System/PublicSystemInfo.cs
@@ -43,7 +43,10 @@ namespace MediaBrowser.Model.System
/// <summary>
/// Gets or sets a value indicating whether the startup wizard is completed.
/// </summary>
- /// <value>The startup completion status.</value>
- public bool StartupWizardCompleted { get; set; }
+ /// <remarks>
+ /// Nullable for OpenAPI specification only to retain backwards compatibility in apiclients.
+ /// </remarks>
+ /// <value>The startup completion status.</value>]
+ public bool? StartupWizardCompleted { get; set; }
}
}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index a1f01f7e8..363b2633f 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -92,6 +92,8 @@ namespace MediaBrowser.Model.Users
public int LoginAttemptsBeforeLockout { get; set; }
+ public int MaxActiveSessions { get; set; }
+
public bool EnablePublicSharing { get; set; }
public Guid[] BlockedMediaFolders { get; set; }
@@ -144,6 +146,8 @@ namespace MediaBrowser.Model.Users
LoginAttemptsBeforeLockout = -1;
+ MaxActiveSessions = 0;
+
EnableAllChannels = true;
EnabledChannels = Array.Empty<Guid>();
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index b6fb4267f..a0c7d4ad0 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -158,6 +158,14 @@ namespace MediaBrowser.Providers.Manager
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ if (response.StatusCode != HttpStatusCode.OK)
+ {
+ throw new HttpException("Invalid image received.")
+ {
+ StatusCode = response.StatusCode
+ };
+ }
+
var contentType = response.Content.Headers.ContentType.MediaType;
// Workaround for tvheadend channel icons
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 813dd441f..9465fe42c 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,11 +16,12 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
- <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
+ <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.2" />
+ <PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
<PackageReference Include="TvDbSharper" Version="3.2.2" />
</ItemGroup>
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index d231bfa2f..9804ec3bb 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -214,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index abfa1c6e7..31f0123dc 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -46,6 +46,7 @@ namespace MediaBrowser.Providers.Music
private readonly string _musicBrainzBaseUrl;
+ private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1);
private Stopwatch _stopWatchMusicBrainz = new Stopwatch();
public MusicBrainzAlbumProvider(
@@ -742,48 +743,58 @@ namespace MediaBrowser.Providers.Music
/// </summary>
internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
{
- using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url);
+ await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
- // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
- options.Headers.UserAgent.ParseAdd(string.Format(
- CultureInfo.InvariantCulture,
- "{0} ( {1} )",
- _appHost.ApplicationUserAgent,
- _appHost.ApplicationUserAgentAddress));
-
- HttpResponseMessage response;
- var attempts = 0u;
-
- do
+ try
{
- attempts++;
+ HttpResponseMessage response;
+ var attempts = 0u;
+ var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url;
- if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
+ do
{
- // MusicBrainz is extremely adamant about limiting to one request per second
- var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
- await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
- }
+ attempts++;
- // Write time since last request to debug log as evidence we're meeting rate limit
- // requirement, before resetting stopwatch back to zero.
- _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
- _stopWatchMusicBrainz.Restart();
+ if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
+ {
+ // MusicBrainz is extremely adamant about limiting to one request per second.
+ var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
+ await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
+ }
- response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options).ConfigureAwait(false);
+ // Write time since last request to debug log as evidence we're meeting rate limit
+ // requirement, before resetting stopwatch back to zero.
+ _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
+ _stopWatchMusicBrainz.Restart();
- // We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
- }
- while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
+ using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
+
+ // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
+ // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent .
+ request.Headers.UserAgent.ParseAdd(string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} ( {1} )",
+ _appHost.ApplicationUserAgent,
+ _appHost.ApplicationUserAgentAddress));
- // Log error if unable to query MB database due to throttling
- if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
+ response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(request).ConfigureAwait(false);
+
+ // We retry a finite number of times, and only whilst MB is indicating 503 (throttling).
+ }
+ while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
+
+ // Log error if unable to query MB database due to throttling.
+ if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
+ {
+ _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl);
+ }
+
+ return response;
+ }
+ finally
{
- _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri);
+ _apiRequestLock.Release();
}
-
- return response;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
index f22d484ab..ce0dab701 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
@@ -80,32 +80,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken));
}
- public async Task<List<EpisodeRecord>> GetAllEpisodesAsync(int tvdbId, string language,
- CancellationToken cancellationToken)
- {
- // Traverse all episode pages and join them together
- var episodes = new List<EpisodeRecord>();
- var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken)
- .ConfigureAwait(false);
- episodes.AddRange(episodePage.Data);
- if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue)
- {
- return episodes;
- }
-
- int next = episodePage.Links.Next.Value;
- int last = episodePage.Links.Last.Value;
-
- for (var page = next; page <= last; ++page)
- {
- episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken)
- .ConfigureAwait(false);
- episodes.AddRange(episodePage.Data);
- }
-
- return episodes;
- }
-
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(
string imdbId,
string language,
@@ -176,7 +150,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
string language,
CancellationToken cancellationToken)
{
- searchInfo.SeriesProviderIds.TryGetValue(nameof(MetadataProvider.Tvdb),
+ searchInfo.SeriesProviderIds.TryGetValue(
+ nameof(MetadataProvider.Tvdb),
out var seriesTvdbId);
var episodeQuery = new EpisodeQuery();
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
index 5fa8a3e1c..fd72ea4a8 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
@@ -106,7 +106,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
.ConfigureAwait(false);
if (string.IsNullOrEmpty(episodeTvdbId))
{
- _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
+ _logger.LogError(
+ "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId);
return result;
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
index dc3c60dee..a5cd425f6 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
@@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Series).Name },
+ IncludeItemTypes = new[] { nameof(Series) },
PersonIds = new[] { item.Id },
DtoOptions = new DtoOptions(false)
{
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
index ca9b1d738..b34e52235 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
@@ -123,7 +124,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
await _tvdbClientManager
.GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken)
.ConfigureAwait(false);
- MapSeriesToResult(result, seriesResult.Data, metadataLanguage);
+ await MapSeriesToResult(result, seriesResult.Data, metadataLanguage).ConfigureAwait(false);
}
catch (TvDbServerException e)
{
@@ -297,7 +298,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return name.Trim();
}
- private void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
+ private async Task MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
{
Series series = result.Item;
series.SetProviderId(MetadataProvider.Tvdb, tvdbSeries.Id.ToString());
@@ -340,20 +341,21 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
try
{
- var episodeSummary = _tvdbClientManager
- .GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data;
- var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max();
- var episodeQuery = new EpisodeQuery
+ var episodeSummary = await _tvdbClientManager.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
+
+ if (episodeSummary.Data.AiredSeasons.Length != 0)
{
- AiredSeason = maxSeasonNumber
- };
- var episodesPage =
- _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data;
- result.Item.EndDate = episodesPage.Select(e =>
+ var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Max(s => Convert.ToInt32(s, CultureInfo.InvariantCulture));
+ var episodeQuery = new EpisodeQuery
{
- DateTime.TryParse(e.FirstAired, out var firstAired);
- return firstAired;
- }).Max();
+ AiredSeason = maxSeasonNumber
+ };
+ var episodesPage = await _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
+
+ result.Item.EndDate = episodesPage.Data
+ .Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null)
+ .Max();
+ }
}
catch (TvDbServerException e)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index f6592afe4..df1e12240 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -12,25 +13,25 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory)
+ public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
_httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
- public string Name => ProviderName;
+ public string Name => TmdbUtils.ProviderName;
- public static string ProviderName => TmdbUtils.ProviderName;
+ public int Order => 0;
public bool Supports(BaseItem item)
{
@@ -48,112 +49,60 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
- var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
+ var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!string.IsNullOrEmpty(tmdbId))
+ if (tmdbId <= 0)
{
- var language = item.GetPreferredMetadataLanguage();
-
- var mainResult = await TmdbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false);
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- if (mainResult != null)
- {
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+ var language = item.GetPreferredMetadataLanguage();
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+ var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
- return GetImages(mainResult, language, tmdbImageUrl);
- }
+ if (collection?.Images == null)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
}
- return new List<RemoteImageInfo>();
- }
+ var remoteImages = new List<RemoteImageInfo>();
- private IEnumerable<RemoteImageInfo> GetImages(CollectionResult obj, string language, string baseUrl)
- {
- var list = new List<RemoteImageInfo>();
-
- var images = obj.Images ?? new CollectionImages();
-
- list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo
- {
- Url = baseUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- }));
-
- list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo
+ for (var i = 0; i < collection.Images.Posters.Count; i++)
{
- Url = baseUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- }));
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
- {
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
+ var poster = collection.Images.Posters[i];
+ remoteImages.Add(new RemoteImageInfo
{
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
+ Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
+ CommunityRating = poster.VoteAverage,
+ VoteCount = poster.VoteCount,
+ Width = poster.Width,
+ Height = poster.Height,
+ Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ });
+ }
- if (string.IsNullOrEmpty(i.Language))
+ for (var i = 0; i < collection.Images.Backdrops.Count; i++)
+ {
+ var backdrop = collection.Images.Backdrops[i];
+ remoteImages.Add(new RemoteImageInfo
{
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- /// <summary>
- /// Gets the posters.
- /// </summary>
- /// <param name="images">The images.</param>
- /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns>
- private IEnumerable<Poster> GetPosters(CollectionImages images)
- {
- return images.Posters ?? new List<Poster>();
- }
-
- /// <summary>
- /// Gets the backdrops.
- /// </summary>
- /// <param name="images">The images.</param>
- /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns>
- private IEnumerable<Backdrop> GetBackdrops(CollectionImages images)
- {
- var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() :
- images.Backdrops;
+ Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
+ CommunityRating = backdrop.VoteAverage,
+ VoteCount = backdrop.VoteCount,
+ Width = backdrop.Width,
+ Height = backdrop.Height,
+ ProviderName = Name,
+ Type = ImageType.Backdrop,
+ RatingType = RatingType.Score
+ });
+ }
- return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
- .ThenByDescending(i => i.Vote_Count);
+ return remoteImages.OrderByLanguageDescending(language);
}
- public int Order => 0;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index e7328b553..fcd8e614c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -3,268 +3,116 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
public class TmdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo>
{
- private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images";
-
- internal static TmdbBoxSetProvider Current;
-
- private readonly ILogger<TmdbBoxSetProvider> _logger;
- private readonly IJsonSerializer _json;
- private readonly IServerConfigurationManager _config;
- private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILibraryManager _libraryManager;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbBoxSetProvider(
- ILogger<TmdbBoxSetProvider> logger,
- IJsonSerializer json,
- IServerConfigurationManager config,
- IFileSystem fileSystem,
- IHttpClientFactory httpClientFactory,
- ILibraryManager libraryManager)
+ public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _logger = logger;
- _json = json;
- _config = config;
- _fileSystem = fileSystem;
_httpClientFactory = httpClientFactory;
- _libraryManager = libraryManager;
- Current = this;
+ _tmdbClientManager = tmdbClientManager;
}
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ public string Name => TmdbUtils.ProviderName;
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken)
{
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+ var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ var language = searchInfo.MetadataLanguage;
- if (!string.IsNullOrEmpty(tmdbId))
+ if (tmdbId > 0)
{
- await EnsureInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage);
- var info = _json.DeserializeFromFile<CollectionResult>(dataFilePath);
-
- var images = (info.Images ?? new CollectionImages()).Posters ?? new List<Poster>();
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+ var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+ if (collection == null)
+ {
+ return Enumerable.Empty<RemoteSearchResult>();
+ }
var result = new RemoteSearchResult
{
- Name = info.Name,
- SearchProviderName = Name,
- ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
+ Name = collection.Name,
+ SearchProviderName = Name
};
- result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
-
- return new[] { result };
- }
-
- return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
- }
-
- public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
- {
- var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
-
- // We don't already have an Id, need to fetch it
- if (string.IsNullOrEmpty(tmdbId))
- {
- var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false);
-
- var searchResult = searchResults.FirstOrDefault();
-
- if (searchResult != null)
- {
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
- }
- }
-
- var result = new MetadataResult<BoxSet>();
-
- if (!string.IsNullOrEmpty(tmdbId))
- {
- var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (mainResult != null)
+ if (collection.Images != null)
{
- result.HasMetadata = true;
- result.Item = GetItem(mainResult);
+ result.ImageUrl = _tmdbClientManager.GetPosterUrl(collection.PosterPath);
}
- }
- return result;
- }
+ result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
- internal async Task<CollectionResult> GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
+ return new[] { result };
}
- await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language);
+ var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
- if (!string.IsNullOrEmpty(dataFilePath))
+ var collections = new List<RemoteSearchResult>();
+ for (var i = 0; i < collectionSearchResults.Count; i++)
{
- return _json.DeserializeFromFile<CollectionResult>(dataFilePath);
- }
-
- return null;
- }
-
- private BoxSet GetItem(CollectionResult obj)
- {
- var item = new BoxSet
- {
- Name = obj.Name,
- Overview = obj.Overview
- };
-
- item.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
-
- return item;
- }
-
- private async Task DownloadInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var mainResult = await FetchMainResult(tmdbId, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
+ var collection = new RemoteSearchResult
+ {
+ Name = collectionSearchResults[i].Name,
+ SearchProviderName = Name
+ };
+ collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture));
- if (mainResult == null)
- {
- return;
+ collections.Add(collection);
}
- var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- _json.SerializeToFile(mainResult, dataFilePath);
+ return collections;
}
- private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
+ public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
{
- var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
+ var tmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ var language = id.MetadataLanguage;
+ // We don't already have an Id, need to fetch it
+ if (tmdbId <= 0)
{
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
+ var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false);
- // Get images in english and with no language
- url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
+ if (searchResults != null && searchResults.Count > 0)
+ {
+ tmdbId = searchResults[0].Id;
+ }
}
- cancellationToken.ThrowIfCancellationRequested();
+ var result = new MetadataResult<BoxSet>();
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
+ if (tmdbId > 0)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false);
+ var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
- cancellationToken.ThrowIfCancellationRequested();
-
- if (mainResult != null && string.IsNullOrEmpty(mainResult.Name))
- {
- if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+ if (collection != null)
{
- url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
-
- if (!string.IsNullOrEmpty(language))
+ var item = new BoxSet
{
- // Get images in english and with no language
- url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
- }
-
- using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(langStream).ConfigureAwait(false);
- }
- }
+ Name = collection.Name,
+ Overview = collection.Overview
+ };
- return mainResult;
- }
-
- internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
+ item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
-
- if (fileInfo.Exists)
- {
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
- {
- return Task.CompletedTask;
+ result.HasMetadata = true;
+ result.Item = item;
}
}
- return DownloadInfo(tmdbId, preferredMetadataLanguage, cancellationToken);
- }
-
- public string Name => TmdbUtils.ProviderName;
-
- private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage)
- {
- var path = GetDataPath(appPaths, tmdbId);
-
- var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage ?? string.Empty);
-
- return Path.Combine(path, filename);
- }
-
- private static string GetDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var dataPath = GetCollectionsDataPath(appPaths);
-
- return Path.Combine(dataPath, tmdbId);
- }
-
- private static string GetCollectionsDataPath(IApplicationPaths appPaths)
- {
- var dataPath = Path.Combine(appPaths.CachePath, "tmdb-collections");
-
- return dataPath;
+ return result;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs
deleted file mode 100644
index 0a8994d54..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
-{
- public class CollectionImages
- {
- public List<Backdrop> Backdrops { get; set; }
-
- public List<Poster> Posters { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs
deleted file mode 100644
index c6b851c23..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
-{
- public class CollectionResult
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Overview { get; set; }
-
- public string Poster_Path { get; set; }
-
- public string Backdrop_Path { get; set; }
-
- public List<Part> Parts { get; set; }
-
- public CollectionImages Images { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs
deleted file mode 100644
index a48124b3e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
-{
- public class Part
- {
- public string Title { get; set; }
-
- public int Id { get; set; }
-
- public string Release_Date { get; set; }
-
- public string Poster_Path { get; set; }
-
- public string Backdrop_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs
deleted file mode 100644
index 5b7627f6e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Backdrop
- {
- public double Aspect_Ratio { get; set; }
-
- public string File_Path { get; set; }
-
- public int Height { get; set; }
-
- public string Iso_639_1 { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public int Width { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs
deleted file mode 100644
index 339ecb628..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Crew
- {
- public int Id { get; set; }
-
- public string Credit_Id { get; set; }
-
- public string Name { get; set; }
-
- public string Department { get; set; }
-
- public string Job { get; set; }
-
- public string Profile_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
deleted file mode 100644
index aac4420e8..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class ExternalIds
- {
- public string Imdb_Id { get; set; }
-
- public object Freebase_Id { get; set; }
-
- public string Freebase_Mid { get; set; }
-
- public int? Tvdb_Id { get; set; }
-
- public int? Tvrage_Id { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs
deleted file mode 100644
index 9ba1c15c6..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Genre
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs
deleted file mode 100644
index 0538cf174..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Images
- {
- public List<Backdrop> Backdrops { get; set; }
-
- public List<Poster> Posters { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs
deleted file mode 100644
index fff86931b..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Keyword
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs
deleted file mode 100644
index 235ecb568..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Keywords
- {
- public List<Keyword> Results { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs
deleted file mode 100644
index 4f61e978b..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Poster
- {
- public double Aspect_Ratio { get; set; }
-
- public string File_Path { get; set; }
-
- public int Height { get; set; }
-
- public string Iso_639_1 { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public int Width { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs
deleted file mode 100644
index 0a1f8843e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Profile
- {
- public string File_Path { get; set; }
-
- public int Width { get; set; }
-
- public int Height { get; set; }
-
- public object Iso_639_1 { get; set; }
-
- public double Aspect_Ratio { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs
deleted file mode 100644
index 61de819b9..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Still
- {
- public double Aspect_Ratio { get; set; }
-
- public string File_Path { get; set; }
-
- public int Height { get; set; }
-
- public string Id { get; set; }
-
- public string Iso_639_1 { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public int Width { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs
deleted file mode 100644
index 59ab18b7b..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class StillImages
- {
- public List<Still> Stills { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs
deleted file mode 100644
index ebd5c7ace..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Video
- {
- public string Id { get; set; }
-
- public string Iso_639_1 { get; set; }
-
- public string Iso_3166_1 { get; set; }
-
- public string Key { get; set; }
-
- public string Name { get; set; }
-
- public string Site { get; set; }
-
- public string Size { get; set; }
-
- public string Type { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
deleted file mode 100644
index 1c673fdbd..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
- public class Videos
- {
- public IReadOnlyList<Video> Results { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs
deleted file mode 100644
index e8745be14..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class BelongsToCollection
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Poster_Path { get; set; }
-
- public string Backdrop_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs
deleted file mode 100644
index 937cfb8f6..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class Cast
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Character { get; set; }
-
- public int Order { get; set; }
-
- public int Cast_Id { get; set; }
-
- public string Profile_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs
deleted file mode 100644
index 37547640f..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class Casts
- {
- public List<Cast> Cast { get; set; }
-
- public List<Crew> Crew { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs
deleted file mode 100644
index edd656a46..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class Country
- {
- public string Iso_3166_1 { get; set; }
-
- public string Certification { get; set; }
-
- public DateTime Release_Date { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
deleted file mode 100644
index 704ebcd5a..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class MovieResult
- {
- public bool Adult { get; set; }
-
- public string Backdrop_Path { get; set; }
-
- public BelongsToCollection Belongs_To_Collection { get; set; }
-
- public long Budget { get; set; }
-
- public List<Genre> Genres { get; set; }
-
- public string Homepage { get; set; }
-
- public int Id { get; set; }
-
- public string Imdb_Id { get; set; }
-
- public string Original_Title { get; set; }
-
- public string Original_Name { get; set; }
-
- public string Overview { get; set; }
-
- public double Popularity { get; set; }
-
- public string Poster_Path { get; set; }
-
- public List<ProductionCompany> Production_Companies { get; set; }
-
- public List<ProductionCountry> Production_Countries { get; set; }
-
- public string Release_Date { get; set; }
-
- public long Revenue { get; set; }
-
- public int Runtime { get; set; }
-
- public List<SpokenLanguage> Spoken_Languages { get; set; }
-
- public string Status { get; set; }
-
- public string Tagline { get; set; }
-
- public string Title { get; set; }
-
- public string Name { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public Casts Casts { get; set; }
-
- public Releases Releases { get; set; }
-
- public Images Images { get; set; }
-
- public Keywords Keywords { get; set; }
-
- public Trailers Trailers { get; set; }
-
- public string GetOriginalTitle()
- {
- return Original_Name ?? Original_Title;
- }
-
- public string GetTitle()
- {
- return Name ?? Title ?? GetOriginalTitle();
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs
deleted file mode 100644
index 2788731b2..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class ProductionCompany
- {
- public string Name { get; set; }
-
- public int Id { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs
deleted file mode 100644
index 1b6f2cc67..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class ProductionCountry
- {
- public string Iso_3166_1 { get; set; }
-
- public string Name { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs
deleted file mode 100644
index 276fbaaf5..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class Releases
- {
- public List<Country> Countries { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs
deleted file mode 100644
index 67231d219..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class SpokenLanguage
- {
- public string Iso_639_1 { get; set; }
-
- public string Name { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
deleted file mode 100644
index 057177294..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class Trailers
- {
- public IReadOnlyList<Youtube> Youtube { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs
deleted file mode 100644
index 6885b7dab..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
- public class Youtube
- {
- public string Name { get; set; }
-
- public string Size { get; set; }
-
- public string Source { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
deleted file mode 100644
index d82e0fc6d..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
-{
- public class PersonImages
- {
- public IReadOnlyList<Profile> Profiles { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs
deleted file mode 100644
index 460ced49a..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
-{
- public class PersonResult
- {
- public bool Adult { get; set; }
-
- public List<string> Also_Known_As { get; set; }
-
- public string Biography { get; set; }
-
- public string Birthday { get; set; }
-
- public string Deathday { get; set; }
-
- public string Homepage { get; set; }
-
- public int Id { get; set; }
-
- public string Imdb_Id { get; set; }
-
- public string Name { get; set; }
-
- public string Place_Of_Birth { get; set; }
-
- public double Popularity { get; set; }
-
- public string Profile_Path { get; set; }
-
- public PersonImages Images { get; set; }
-
- public ExternalIds External_Ids { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs
deleted file mode 100644
index 87c2a723d..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
- public class ExternalIdLookupResult
- {
- public List<TvResult> Tv_Results { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs
deleted file mode 100644
index 401c75c31..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
- public class MovieResult
- {
- /// <summary>
- /// Gets or sets a value indicating whether this <see cref="MovieResult" /> is adult.
- /// </summary>
- /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
- public bool Adult { get; set; }
-
- /// <summary>
- /// Gets or sets the backdrop_path.
- /// </summary>
- /// <value>The backdrop_path.</value>
- public string Backdrop_Path { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public int Id { get; set; }
-
- /// <summary>
- /// Gets or sets the original_title.
- /// </summary>
- /// <value>The original_title.</value>
- public string Original_Title { get; set; }
-
- /// <summary>
- /// Gets or sets the original_name.
- /// </summary>
- /// <value>The original_name.</value>
- public string Original_Name { get; set; }
-
- /// <summary>
- /// Gets or sets the release_date.
- /// </summary>
- /// <value>The release_date.</value>
- public string Release_Date { get; set; }
-
- /// <summary>
- /// Gets or sets the poster_path.
- /// </summary>
- /// <value>The poster_path.</value>
- public string Poster_Path { get; set; }
-
- /// <summary>
- /// Gets or sets the popularity.
- /// </summary>
- /// <value>The popularity.</value>
- public double Popularity { get; set; }
-
- /// <summary>
- /// Gets or sets the title.
- /// </summary>
- /// <value>The title.</value>
- public string Title { get; set; }
-
- /// <summary>
- /// Gets or sets the vote_average.
- /// </summary>
- /// <value>The vote_average.</value>
- public double Vote_Average { get; set; }
-
- /// <summary>
- /// For collection search results.
- /// </summary>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the vote_count.
- /// </summary>
- /// <value>The vote_count.</value>
- public int Vote_Count { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs
deleted file mode 100644
index 4cff45ca6..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
- public class PersonSearchResult
- {
- /// <summary>
- /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult.
- /// </summary>
- /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
- public bool Adult { get; set; }
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public int Id { get; set; }
-
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the profile_ path.
- /// </summary>
- /// <value>The profile_ path.</value>
- public string Profile_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs
deleted file mode 100644
index 3b9257b62..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
- public class TmdbSearchResult<T>
- {
- /// <summary>
- /// Gets or sets the page.
- /// </summary>
- /// <value>The page.</value>
- public int Page { get; set; }
-
- /// <summary>
- /// Gets or sets the results.
- /// </summary>
- /// <value>The results.</value>
- public List<T> Results { get; set; }
-
- /// <summary>
- /// Gets or sets the total_pages.
- /// </summary>
- /// <value>The total_pages.</value>
- public int Total_Pages { get; set; }
-
- /// <summary>
- /// Gets or sets the total_results.
- /// </summary>
- /// <value>The total_results.</value>
- public int Total_Results { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs
deleted file mode 100644
index b2bb068b5..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
- public class TvResult
- {
- public string Backdrop_Path { get; set; }
-
- public string First_Air_Date { get; set; }
-
- public int Id { get; set; }
-
- public string Original_Name { get; set; }
-
- public string Poster_Path { get; set; }
-
- public double Popularity { get; set; }
-
- public string Name { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs
deleted file mode 100644
index 4ce26c65e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Cast
- {
- public string Character { get; set; }
-
- public string Credit_Id { get; set; }
-
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Profile_Path { get; set; }
-
- public int Order { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs
deleted file mode 100644
index aef4e2863..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class ContentRating
- {
- public string Iso_3166_1 { get; set; }
-
- public string Rating { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs
deleted file mode 100644
index ae1b5668d..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class ContentRatings
- {
- public List<ContentRating> Results { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs
deleted file mode 100644
index ba36632e0..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class CreatedBy
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Profile_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs
deleted file mode 100644
index 47205d875..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Credits
- {
- public List<Cast> Cast { get; set; }
-
- public List<Crew> Crew { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs
deleted file mode 100644
index 53e3c2695..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Episode
- {
- public string Air_Date { get; set; }
-
- public int Episode_Number { get; set; }
-
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Overview { get; set; }
-
- public string Still_Path { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs
deleted file mode 100644
index 9707e4bf4..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class EpisodeCredits
- {
- public List<Cast> Cast { get; set; }
-
- public List<Crew> Crew { get; set; }
-
- public List<GuestStar> Guest_Stars { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs
deleted file mode 100644
index 4458bad36..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class EpisodeResult
- {
- public DateTime Air_Date { get; set; }
-
- public int Episode_Number { get; set; }
-
- public string Name { get; set; }
-
- public string Overview { get; set; }
-
- public int Id { get; set; }
-
- public object Production_Code { get; set; }
-
- public int Season_Number { get; set; }
-
- public string Still_Path { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public StillImages Images { get; set; }
-
- public ExternalIds External_Ids { get; set; }
-
- public EpisodeCredits Credits { get; set; }
-
- public Tmdb.Models.General.Videos Videos { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs
deleted file mode 100644
index 8f3988641..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class GuestStar
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Credit_Id { get; set; }
-
- public string Character { get; set; }
-
- public int Order { get; set; }
-
- public string Profile_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs
deleted file mode 100644
index 3dc310d33..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Network
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs
deleted file mode 100644
index 9cbd283a9..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Season
- {
- public string Air_Date { get; set; }
-
- public int Episode_Count { get; set; }
-
- public int Id { get; set; }
-
- public string Poster_Path { get; set; }
-
- public int Season_Number { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs
deleted file mode 100644
index f364d4921..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class SeasonImages
- {
- public List<Poster> Posters { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs
deleted file mode 100644
index e98048eac..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class SeasonResult
- {
- public DateTime Air_Date { get; set; }
-
- public List<Episode> Episodes { get; set; }
-
- public string Name { get; set; }
-
- public string Overview { get; set; }
-
- public int Id { get; set; }
-
- public string Poster_Path { get; set; }
-
- public int Season_Number { get; set; }
-
- public Credits Credits { get; set; }
-
- public SeasonImages Images { get; set; }
-
- public ExternalIds External_Ids { get; set; }
-
- public General.Videos Videos { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs
deleted file mode 100644
index 331cd59fa..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class SeriesResult
- {
- public string Backdrop_Path { get; set; }
-
- public List<CreatedBy> Created_By { get; set; }
-
- public List<int> Episode_Run_Time { get; set; }
-
- public DateTime First_Air_Date { get; set; }
-
- public List<Genre> Genres { get; set; }
-
- public string Homepage { get; set; }
-
- public int Id { get; set; }
-
- public bool In_Production { get; set; }
-
- public List<string> Languages { get; set; }
-
- public DateTime Last_Air_Date { get; set; }
-
- public string Name { get; set; }
-
- public List<Network> Networks { get; set; }
-
- public int Number_Of_Episodes { get; set; }
-
- public int Number_Of_Seasons { get; set; }
-
- public string Original_Name { get; set; }
-
- public List<string> Origin_Country { get; set; }
-
- public string Overview { get; set; }
-
- public string Popularity { get; set; }
-
- public string Poster_Path { get; set; }
-
- public List<Season> Seasons { get; set; }
-
- public string Status { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public Credits Credits { get; set; }
-
- public Images Images { get; set; }
-
- public Keywords Keywords { get; set; }
-
- public ExternalIds External_Ids { get; set; }
-
- public General.Videos Videos { get; set; }
-
- public ContentRatings Content_Ratings { get; set; }
-
- public string ResultLanguage { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
deleted file mode 100644
index 3c626f9eb..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
+++ /dev/null
@@ -1,309 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- public class GenericTmdbMovieInfo<T>
- where T : BaseItem, new()
- {
- private readonly ILogger _logger;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly ILibraryManager _libraryManager;
- private readonly IFileSystem _fileSystem;
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- public GenericTmdbMovieInfo(ILogger logger, IJsonSerializer jsonSerializer, ILibraryManager libraryManager, IFileSystem fileSystem)
- {
- _logger = logger;
- _jsonSerializer = jsonSerializer;
- _libraryManager = libraryManager;
- _fileSystem = fileSystem;
- }
-
- public async Task<MetadataResult<T>> GetMetadata(ItemLookupInfo itemId, CancellationToken cancellationToken)
- {
- var tmdbId = itemId.GetProviderId(MetadataProvider.Tmdb);
- var imdbId = itemId.GetProviderId(MetadataProvider.Imdb);
-
- // Don't search for music video id's because it is very easy to misidentify.
- if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId) && typeof(T) != typeof(MusicVideo))
- {
- var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(itemId, cancellationToken).ConfigureAwait(false);
-
- var searchResult = searchResults.FirstOrDefault();
-
- if (searchResult != null)
- {
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
- }
- }
-
- if (!string.IsNullOrEmpty(tmdbId) || !string.IsNullOrEmpty(imdbId))
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- return await FetchMovieData(tmdbId, imdbId, itemId.MetadataLanguage, itemId.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- }
-
- return new MetadataResult<T>();
- }
-
- /// <summary>
- /// Fetches the movie data.
- /// </summary>
- /// <param name="tmdbId">The TMDB identifier.</param>
- /// <param name="imdbId">The imdb identifier.</param>
- /// <param name="language">The language.</param>
- /// <param name="preferredCountryCode">The preferred country code.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{`0}.</returns>
- private async Task<MetadataResult<T>> FetchMovieData(string tmdbId, string imdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
- {
- var item = new MetadataResult<T>
- {
- Item = new T()
- };
-
- string dataFilePath = null;
- MovieResult movieInfo = null;
-
- // Id could be ImdbId or TmdbId
- if (string.IsNullOrEmpty(tmdbId))
- {
- movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false);
- if (movieInfo != null)
- {
- tmdbId = movieInfo.Id.ToString(_usCulture);
-
- dataFilePath = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(movieInfo, dataFilePath);
- }
- }
-
- if (!string.IsNullOrWhiteSpace(tmdbId))
- {
- await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- dataFilePath = dataFilePath ?? TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
- movieInfo = movieInfo ?? _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath);
-
- var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- ProcessMainInfo(item, settings, preferredCountryCode, movieInfo);
- item.HasMetadata = true;
- }
-
- return item;
- }
-
- /// <summary>
- /// Processes the main info.
- /// </summary>
- /// <param name="resultItem">The result item.</param>
- /// <param name="settings">The settings.</param>
- /// <param name="preferredCountryCode">The preferred country code.</param>
- /// <param name="movieData">The movie data.</param>
- private void ProcessMainInfo(MetadataResult<T> resultItem, TmdbSettingsResult settings, string preferredCountryCode, MovieResult movieData)
- {
- var movie = resultItem.Item;
-
- movie.Name = movieData.GetTitle() ?? movie.Name;
-
- movie.OriginalTitle = movieData.GetOriginalTitle();
-
- movie.Overview = string.IsNullOrWhiteSpace(movieData.Overview) ? null : WebUtility.HtmlDecode(movieData.Overview);
- movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
-
- // movie.HomePageUrl = movieData.homepage;
-
- if (!string.IsNullOrEmpty(movieData.Tagline))
- {
- movie.Tagline = movieData.Tagline;
- }
-
- if (movieData.Production_Countries != null)
- {
- movie.ProductionLocations = movieData
- .Production_Countries
- .Select(i => i.Name)
- .ToArray();
- }
-
- movie.SetProviderId(MetadataProvider.Tmdb, movieData.Id.ToString(_usCulture));
- movie.SetProviderId(MetadataProvider.Imdb, movieData.Imdb_Id);
-
- if (movieData.Belongs_To_Collection != null)
- {
- movie.SetProviderId(MetadataProvider.TmdbCollection,
- movieData.Belongs_To_Collection.Id.ToString(CultureInfo.InvariantCulture));
-
- if (movie is Movie movieItem)
- {
- movieItem.CollectionName = movieData.Belongs_To_Collection.Name;
- }
- }
-
- string voteAvg = movieData.Vote_Average.ToString(CultureInfo.InvariantCulture);
-
- if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var rating))
- {
- movie.CommunityRating = rating;
- }
-
- // movie.VoteCount = movieData.vote_count;
-
- if (movieData.Releases != null && movieData.Releases.Countries != null)
- {
- var releases = movieData.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList();
-
- var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
- var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
-
- if (ourRelease != null)
- {
- var ratingPrefix = string.Equals(preferredCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? "" : preferredCountryCode + "-";
- var newRating = ratingPrefix + ourRelease.Certification;
-
- newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase);
-
- movie.OfficialRating = newRating;
- }
- else if (usRelease != null)
- {
- movie.OfficialRating = usRelease.Certification;
- }
- }
-
- if (!string.IsNullOrWhiteSpace(movieData.Release_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParse(movieData.Release_Date, _usCulture, DateTimeStyles.None, out var r))
- {
- movie.PremiereDate = r.ToUniversalTime();
- movie.ProductionYear = movie.PremiereDate.Value.Year;
- }
- }
-
- // studios
- if (movieData.Production_Companies != null)
- {
- movie.SetStudios(movieData.Production_Companies.Select(c => c.Name));
- }
-
- // genres
- // Movies get this from imdb
- var genres = movieData.Genres ?? new List<Tmdb.Models.General.Genre>();
-
- foreach (var genre in genres.Select(g => g.Name))
- {
- movie.AddGenre(genre);
- }
-
- resultItem.ResetPeople();
- var tmdbImageUrl = settings.images.GetImageUrl("original");
-
- // Actors, Directors, Writers - all in People
- // actors come from cast
- if (movieData.Casts != null && movieData.Casts.Cast != null)
- {
- foreach (var actor in movieData.Casts.Cast.OrderBy(a => a.Order))
- {
- var personInfo = new PersonInfo
- {
- Name = actor.Name.Trim(),
- Role = actor.Character,
- Type = PersonType.Actor,
- SortOrder = actor.Order
- };
-
- if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
- }
-
- if (actor.Id > 0)
- {
- personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
- }
-
- resultItem.AddPerson(personInfo);
- }
- }
-
- // and the rest from crew
- if (movieData.Casts?.Crew != null)
- {
- var keepTypes = new[]
- {
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
- };
-
- foreach (var person in movieData.Casts.Crew)
- {
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
-
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
- !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var personInfo = new PersonInfo
- {
- Name = person.Name.Trim(),
- Role = person.Job,
- Type = type
- };
-
- if (!string.IsNullOrWhiteSpace(person.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + person.Profile_Path;
- }
-
- if (person.Id > 0)
- {
- personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
- }
-
- resultItem.AddPerson(personInfo);
- }
- }
-
- // if (movieData.keywords != null && movieData.keywords.keywords != null)
- //{
- // movie.Keywords = movieData.keywords.keywords.Select(i => i.name).ToList();
- //}
-
- if (movieData.Trailers != null && movieData.Trailers.Youtube != null)
- {
- movie.RemoteTrailers = movieData.Trailers.Youtube.Select(i => new MediaUrl
- {
- Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", i.Source),
- Name = i.Name
- }).ToArray();
- }
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
deleted file mode 100644
index 9db7e0997..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- public class TmdbImageProvider : IRemoteImageProvider, IHasOrder
- {
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
-
- public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
- {
- _jsonSerializer = jsonSerializer;
- _httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- }
-
- public string Name => ProviderName;
-
- public static string ProviderName => TmdbUtils.ProviderName;
-
- /// <inheritdoc />
- public int Order => 0;
-
- public bool Supports(BaseItem item)
- {
- return item is Movie || item is MusicVideo || item is Trailer;
- }
-
- public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
- {
- return new List<ImageType>
- {
- ImageType.Primary,
- ImageType.Backdrop
- };
- }
-
- public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
- {
- var list = new List<RemoteImageInfo>();
-
- var language = item.GetPreferredMetadataLanguage();
-
- var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false);
-
- if (results == null)
- {
- return list;
- }
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var supportedImages = GetSupportedImages(item).ToList();
-
- if (supportedImages.Contains(ImageType.Primary))
- {
- list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- }));
- }
-
- if (supportedImages.Contains(ImageType.Backdrop))
- {
- list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- }));
- }
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
- {
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- /// <summary>
- /// Gets the posters.
- /// </summary>
- /// <param name="images">The images.</param>
- /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns>
- private IEnumerable<Poster> GetPosters(Images images)
- {
- return images.Posters ?? new List<Poster>();
- }
-
- /// <summary>
- /// Gets the backdrops.
- /// </summary>
- /// <param name="images">The images.</param>
- /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns>
- private IEnumerable<Backdrop> GetBackdrops(Images images)
- {
- var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() :
- images.Backdrops;
-
- return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
- .ThenByDescending(i => i.Vote_Count);
- }
-
- /// <summary>
- /// Fetches the images.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="language">The language.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{MovieImages}.</returns>
- private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken)
- {
- var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
-
- if (string.IsNullOrWhiteSpace(tmdbId))
- {
- var imdbId = item.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrWhiteSpace(imdbId))
- {
- var movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false);
- if (movieInfo != null)
- {
- tmdbId = movieInfo.Id.ToString(CultureInfo.InvariantCulture);
- }
- }
- }
-
- if (string.IsNullOrWhiteSpace(tmdbId))
- {
- return null;
- }
-
- await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- var path = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
-
- if (!string.IsNullOrEmpty(path))
- {
- var fileInfo = _fileSystem.GetFileInfo(path);
-
- if (fileInfo.Exists)
- {
- return jsonSerializer.DeserializeFromFile<MovieResult>(path).Images;
- }
- }
-
- return null;
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
deleted file mode 100644
index 1ba8f9072..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- internal class TmdbImageSettings
- {
- public IReadOnlyList<string> backdrop_sizes { get; set; }
-
- public string secure_base_url { get; set; }
-
- public IReadOnlyList<string> poster_sizes { get; set; }
-
- public IReadOnlyList<string> profile_sizes { get; set; }
-
- public string GetImageUrl(string image)
- {
- return secure_base_url + image;
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
index 9610e4058..f1a1b65d8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -1,4 +1,3 @@
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
@@ -33,7 +32,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return true;
}
- return item is Movie || item is MusicVideo || item is Trailer;
+ return item is Movie;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
new file mode 100644
index 000000000..dac9e961c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -0,0 +1,128 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Providers;
+using TMDbLib.Objects.Find;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ public class TmdbMovieImageProvider : IRemoteImageProvider, IHasOrder
+ {
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbMovieImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
+ {
+ _httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
+ }
+
+ public int Order => 0;
+
+ public string Name => TmdbUtils.ProviderName;
+
+ public bool Supports(BaseItem item)
+ {
+ return item is Movie || item is Trailer;
+ }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary,
+ ImageType.Backdrop
+ };
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var language = item.GetPreferredMetadataLanguage();
+
+ var movieTmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ if (movieTmdbId <= 0)
+ {
+ var movieImdbId = item.GetProviderId(MetadataProvider.Imdb);
+ if (string.IsNullOrEmpty(movieImdbId))
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var movieResult = await _tmdbClientManager.FindByExternalIdAsync(movieImdbId, FindExternalSource.Imdb, language, cancellationToken).ConfigureAwait(false);
+ if (movieResult?.MovieResults != null && movieResult.MovieResults.Count > 0)
+ {
+ movieTmdbId = movieResult.MovieResults[0].Id;
+ }
+ }
+
+ if (movieTmdbId <= 0)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var movie = await _tmdbClientManager
+ .GetMovieAsync(movieTmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
+
+ if (movie?.Images == null)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var remoteImages = new List<RemoteImageInfo>();
+
+ for (var i = 0; i < movie.Images.Posters.Count; i++)
+ {
+ var poster = movie.Images.Posters[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
+ CommunityRating = poster.VoteAverage,
+ VoteCount = poster.VoteCount,
+ Width = poster.Width,
+ Height = poster.Height,
+ Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ });
+ }
+
+ for (var i = 0; i < movie.Images.Backdrops.Count; i++)
+ {
+ var backdrop = movie.Images.Backdrops[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ Url = _tmdbClientManager.GetPosterUrl(backdrop.FilePath),
+ CommunityRating = backdrop.VoteAverage,
+ VoteCount = backdrop.VoteCount,
+ Width = backdrop.Width,
+ Height = backdrop.Height,
+ ProviderName = Name,
+ Type = ImageType.Backdrop,
+ RatingType = RatingType.Score
+ });
+ }
+
+ return remoteImages.OrderByLanguageDescending(language);
+ }
+
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 31cfd8649..3984e4953 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -3,26 +3,17 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using System.Net;
+using System.Linq;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
@@ -31,365 +22,273 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// </summary>
public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder
{
- private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
- private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly ILogger<TmdbMovieProvider> _logger;
private readonly ILibraryManager _libraryManager;
- private readonly IApplicationHost _appHost;
-
- /// <summary>
- /// The _TMDB settings task.
- /// </summary>
- private TmdbSettingsResult _tmdbSettings;
+ private readonly TmdbClientManager _tmdbClientManager;
public TmdbMovieProvider(
- IJsonSerializer jsonSerializer,
- IHttpClientFactory httpClientFactory,
- IFileSystem fileSystem,
- IServerConfigurationManager configurationManager,
- ILogger<TmdbMovieProvider> logger,
ILibraryManager libraryManager,
- IApplicationHost appHost)
+ TmdbClientManager tmdbClientManager,
+ IHttpClientFactory httpClientFactory)
{
- _jsonSerializer = jsonSerializer;
- _httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _logger = logger;
_libraryManager = libraryManager;
- _appHost = appHost;
- Current = this;
+ _tmdbClientManager = tmdbClientManager;
+ _httpClientFactory = httpClientFactory;
}
- internal static TmdbMovieProvider Current { get; private set; }
-
- /// <inheritdoc />
public string Name => TmdbUtils.ProviderName;
/// <inheritdoc />
public int Order => 1;
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
- return GetMovieSearchResults(searchInfo, cancellationToken);
- }
-
- public async Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo searchInfo, CancellationToken cancellationToken)
- {
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+ var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!string.IsNullOrEmpty(tmdbId))
+ if (tmdbId == 0)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- await EnsureMovieInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
-
- var obj = _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath);
-
- var tmdbSettings = await GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var remoteResult = new RemoteSearchResult
+ var movieResults = await _tmdbClientManager
+ .SearchMovieAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+ var remoteSearchResults = new List<RemoteSearchResult>();
+ for (var i = 0; i < movieResults.Count; i++)
{
- Name = obj.GetTitle(),
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(obj.Release_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParse(obj.Release_Date, _usCulture, DateTimeStyles.None, out var r))
+ var movieResult = movieResults[i];
+ var remoteSearchResult = new RemoteSearchResult
{
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
-
- if (!string.IsNullOrWhiteSpace(obj.Imdb_Id))
- {
- remoteResult.SetProviderId(MetadataProvider.Imdb, obj.Imdb_Id);
+ Name = movieResult.Title ?? movieResult.OriginalTitle,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath),
+ Overview = movieResult.Overview,
+ SearchProviderName = Name
+ };
+
+ var releaseDate = movieResult.ReleaseDate?.ToUniversalTime();
+ remoteSearchResult.PremiereDate = releaseDate;
+ remoteSearchResult.ProductionYear = releaseDate?.Year;
+
+ remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture));
+ remoteSearchResults.Add(remoteSearchResult);
}
- return new[] { remoteResult };
+ return remoteSearchResults;
}
- return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
- }
-
- public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
- {
- return GetItemMetadata<Movie>(info, cancellationToken);
- }
+ var movie = await _tmdbClientManager
+ .GetMovieAsync(tmdbId, searchInfo.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- public Task<MetadataResult<T>> GetItemMetadata<T>(ItemLookupInfo id, CancellationToken cancellationToken)
- where T : BaseItem, new()
- {
- var movieDb = new GenericTmdbMovieInfo<T>(_logger, _jsonSerializer, _libraryManager, _fileSystem);
-
- return movieDb.GetMetadata(id, cancellationToken);
- }
-
- /// <summary>
- /// Gets the TMDB settings.
- /// </summary>
- /// <returns>Task{TmdbSettingsResult}.</returns>
- internal async Task<TmdbSettingsResult> GetTmdbSettings(CancellationToken cancellationToken)
- {
- if (_tmdbSettings != null)
+ var remoteResult = new RemoteSearchResult
{
- return _tmdbSettings;
- }
+ Name = movie.Title ?? movie.OriginalTitle,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
+ Overview = movie.Overview
+ };
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey));
- foreach (var header in TmdbUtils.AcceptHeaders)
+ if (movie.ReleaseDate != null)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
+ remoteResult.PremiereDate = releaseDate;
+ remoteResult.ProductionYear = releaseDate.Year;
}
- using var response = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(stream).ConfigureAwait(false);
- return _tmdbSettings;
- }
-
- /// <summary>
- /// Gets the movie data path.
- /// </summary>
- /// <param name="appPaths">The app paths.</param>
- /// <param name="tmdbId">The TMDB id.</param>
- /// <returns>System.String.</returns>
- internal static string GetMovieDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var dataPath = GetMoviesDataPath(appPaths);
-
- return Path.Combine(dataPath, tmdbId);
- }
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
- internal static string GetMoviesDataPath(IApplicationPaths appPaths)
- {
- var dataPath = Path.Combine(appPaths.CachePath, "tmdb-movies2");
-
- return dataPath;
- }
-
- /// <summary>
- /// Downloads the movie info.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- internal async Task DownloadMovieInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var mainResult = await FetchMainResult(id, true, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (mainResult == null)
+ if (!string.IsNullOrWhiteSpace(movie.ImdbId))
{
- return;
+ remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
}
- var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ return new[] { remoteResult };
}
- internal Task EnsureMovieInfo(string tmdbId, string language, CancellationToken cancellationToken)
+ public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- var path = GetDataFilePath(tmdbId, language);
-
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
+ var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
+ var imdbId = info.GetProviderId(MetadataProvider.Imdb);
- if (fileInfo.Exists)
+ if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId))
{
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ // ParseName is required here.
+ // Caller provides the filename with extension stripped and NOT the parsed filename
+ var parsedName = _libraryManager.ParseName(info.Name);
+ var searchResults = await _tmdbClientManager.SearchMovieAsync(parsedName.Name, parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ if (searchResults.Count > 0)
{
- return Task.CompletedTask;
+ tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
- return DownloadMovieInfo(tmdbId, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, string preferredLanguage)
- {
if (string.IsNullOrEmpty(tmdbId))
{
- throw new ArgumentNullException(nameof(tmdbId));
+ return new MetadataResult<Movie>();
}
- var path = GetMovieDataPath(_configurationManager.ApplicationPaths, tmdbId);
+ var movieResult = await _tmdbClientManager
+ .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- if (string.IsNullOrWhiteSpace(preferredLanguage))
+ var movie = new Movie
+ {
+ Name = movieResult.Title ?? movieResult.OriginalTitle,
+ Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
+ Tagline = movieResult.Tagline,
+ ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
+ };
+ var metadataResult = new MetadataResult<Movie>
{
- preferredLanguage = "alllang";
+ HasMetadata = true,
+ ResultLanguage = info.MetadataLanguage,
+ Item = movie
+ };
+
+ movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
+ movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
+ if (movieResult.BelongsToCollection != null)
+ {
+ movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));
+ movie.CollectionName = movieResult.BelongsToCollection.Name;
}
- var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage);
+ movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage);
- return Path.Combine(path, filename);
- }
+ if (movieResult.Releases?.Countries != null)
+ {
+ var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList();
- public static string GetImageLanguagesParam(string preferredLanguage)
- {
- var languages = new List<string>();
+ var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase));
+ var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
- if (!string.IsNullOrEmpty(preferredLanguage))
- {
- preferredLanguage = NormalizeLanguage(preferredLanguage);
+ if (ourRelease != null)
+ {
+ var ratingPrefix = string.Equals(info.MetadataCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? string.Empty : info.MetadataCountryCode + "-";
+ var newRating = ratingPrefix + ourRelease.Certification;
- languages.Add(preferredLanguage);
+ newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase);
- if (preferredLanguage.Length == 5) // like en-US
+ movie.OfficialRating = newRating;
+ }
+ else if (usRelease != null)
{
- // Currenty, TMDB supports 2-letter language codes only
- // They are planning to change this in the future, thus we're
- // supplying both codes if we're having a 5-letter code.
- languages.Add(preferredLanguage.Substring(0, 2));
+ movie.OfficialRating = usRelease.Certification;
}
}
- languages.Add("null");
+ movie.PremiereDate = movieResult.ReleaseDate;
+ movie.ProductionYear = movieResult.ReleaseDate?.Year;
- if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
+ if (movieResult.ProductionCompanies != null)
{
- languages.Add("en");
+ movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name));
}
- return string.Join(',', languages);
- }
+ var genres = movieResult.Genres;
- public static string NormalizeLanguage(string language)
- {
- if (!string.IsNullOrEmpty(language))
+ foreach (var genre in genres.Select(g => g.Name))
{
- // They require this to be uppercase
- // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api.
- // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
- var parts = language.Split('-');
+ movie.AddGenre(genre);
+ }
- if (parts.Length == 2)
+ if (movieResult.Keywords?.Keywords != null)
+ {
+ for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++)
{
- language = parts[0] + "-" + parts[1].ToUpperInvariant();
+ movie.AddTag(movieResult.Keywords.Keywords[i].Name);
}
}
- return language;
- }
-
- public static string AdjustImageLanguage(string imageLanguage, string requestLanguage)
- {
- if (!string.IsNullOrEmpty(imageLanguage)
- && !string.IsNullOrEmpty(requestLanguage)
- && requestLanguage.Length > 2
- && imageLanguage.Length == 2
- && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
+ if (movieResult.Credits?.Cast != null)
{
- return requestLanguage;
- }
-
- return imageLanguage;
- }
+ // TODO configurable
+ foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ {
+ var personInfo = new PersonInfo
+ {
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ };
- /// <summary>
- /// Fetches the main result.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <param name="isTmdbId">if set to <c>true</c> [is TMDB identifier].</param>
- /// <param name="language">The language.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{CompleteMovieData}.</returns>
- internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey);
+ if (!string.IsNullOrWhiteSpace(actor.ProfilePath))
+ {
+ personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath);
+ }
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language));
+ if (actor.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
+ }
- // Get images in english and with no language
- url += "&include_image_language=" + GetImageLanguagesParam(language);
+ metadataResult.AddPerson(personInfo);
+ }
}
- cancellationToken.ThrowIfCancellationRequested();
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
+ if (movieResult.Credits?.Crew != null)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
+ var keepTypes = new[]
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
- using var mainResponse = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- if (mainResponse.StatusCode == HttpStatusCode.NotFound)
- {
- return null;
- }
+ foreach (var person in movieResult.Credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
- await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
+ !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- cancellationToken.ThrowIfCancellationRequested();
+ var personInfo = new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ };
- // If the language preference isn't english, then have the overview fallback to english if it's blank
- if (mainResult != null &&
- string.IsNullOrEmpty(mainResult.Overview) &&
- !string.IsNullOrEmpty(language) &&
- !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English...");
+ if (!string.IsNullOrWhiteSpace(person.ProfilePath))
+ {
+ personInfo.ImageUrl = _tmdbClientManager.GetPosterUrl(person.ProfilePath);
+ }
- url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+ if (person.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
+ }
- if (!string.IsNullOrEmpty(language))
- {
- // Get images in english and with no language
- url += "&include_image_language=" + GetImageLanguagesParam(language);
+ metadataResult.AddPerson(personInfo);
}
+ }
+
- using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
+ if (movieResult.Videos?.Results != null)
+ {
+ var trailers = new List<MediaUrl>();
+ for (var i = 0; i < movieResult.Videos.Results.Count; i++)
{
- langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
+ var video = movieResult.Videos.Results[0];
+ if (!TmdbUtils.IsTrailerType(video))
+ {
+ continue;
+ }
- using var langResponse = await GetMovieDbResponse(langRequestMessage, cancellationToken).ConfigureAwait(false);
+ trailers.Add(new MediaUrl
+ {
+ Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", video.Key),
+ Name = video.Name
+ });
+ }
- await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
- mainResult.Overview = langResult.Overview;
+ movie.RemoteTrailers = trailers;
}
- return mainResult;
- }
-
- /// <summary>
- /// Gets the movie db response.
- /// </summary>
- /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
- internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default)
- {
- message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent);
- return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken);
+ return metadataResult;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
deleted file mode 100644
index 36a4eef8a..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- public class TmdbSearch
- {
- private const string SearchUrl = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
- private const string SearchUrlTvWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/tv?api_key={1}&query={0}&language={2}&first_air_date_year={3}";
- private const string SearchUrlMovieWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/movie?api_key={1}&query={0}&language={2}&primary_release_year={3}";
-
- private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled);
- private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled);
- private static readonly Regex _cleanStopWords = new Regex(
- @"\b( # Start at word boundary
- 19[0-9]{2}|20[0-9]{2}| # 1900-2099
- S[0-9]{2}| # Season
- E[0-9]{2}| # Episode
- (2160|1080|720|576|480)[ip]?| # Resolution
- [xh]?264| # Encoding
- (web|dvd|bd|hdtv|hd)rip| # *Rip
- web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac
- ).* # Match rest of string",
- RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase);
-
- private readonly ILogger _logger;
- private readonly IJsonSerializer _json;
- private readonly ILibraryManager _libraryManager;
-
- public TmdbSearch(ILogger logger, IJsonSerializer json, ILibraryManager libraryManager)
- {
- _logger = logger;
- _json = json;
- _libraryManager = libraryManager;
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "tv", cancellationToken);
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "movie", cancellationToken);
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "collection", cancellationToken);
- }
-
- private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLookupInfo idInfo, string searchType, CancellationToken cancellationToken)
- {
- var name = idInfo.Name;
- var year = idInfo.Year;
-
- if (string.IsNullOrWhiteSpace(name))
- {
- return new List<RemoteSearchResult>();
- }
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- // ParseName is required here.
- // Caller provides the filename with extension stripped and NOT the parsed filename
- var parsedName = _libraryManager.ParseName(name);
- var yearInName = parsedName.Year;
- name = parsedName.Name;
- year ??= yearInName;
-
- var language = idInfo.MetadataLanguage.ToLowerInvariant();
-
- // Replace sequences of non-word characters with space
- // TMDB expects a space separated list of words make sure that is the case
- name = _cleanNonWord.Replace(name, " ").Trim();
-
- _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year);
- var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
-
- if (results.Count == 0)
- {
- // try in english if wasn't before
- if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
- }
- }
-
- // TODO: retrying alternatives should be done outside the search
- // provider so that the retry logic can be common for all search
- // providers
- if (results.Count == 0)
- {
- var name2 = parsedName.Name;
-
- // Remove things enclosed in []{}() etc
- name2 = _cleanEnclosed.Replace(name2, string.Empty);
-
- // Replace sequences of non-word characters with space
- name2 = _cleanNonWord.Replace(name2, " ");
-
- // Clean based on common stop words / tokens
- name2 = _cleanStopWords.Replace(name2, string.Empty);
-
- // Trim whitespace
- name2 = name2.Trim();
-
- // Search again if the new name is different
- if (!string.Equals(name2, name, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(name2))
- {
- _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year);
- results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
-
- if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- // one more time, in english
- results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
- }
- }
- }
-
- return results.Where(i =>
- {
- if (year.HasValue && i.ProductionYear.HasValue)
- {
- // Allow one year tolerance
- return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
- }
-
- return true;
- });
- }
-
- private Task<List<RemoteSearchResult>> GetSearchResults(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
- {
- switch (type)
- {
- case "tv":
- return GetSearchResultsTv(name, year, language, baseImageUrl, cancellationToken);
- default:
- return GetSearchResultsGeneric(name, type, year, language, baseImageUrl, cancellationToken);
- }
- }
-
- private async Task<List<RemoteSearchResult>> GetSearchResultsGeneric(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("String can't be null or empty.", nameof(name));
- }
-
- string url3;
- if (year != null && string.Equals(type, "movie", StringComparison.OrdinalIgnoreCase))
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrlMovieWithYear,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- year);
- }
- else
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrl,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- type);
- }
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(stream).ConfigureAwait(false);
-
- var results = searchResults.Results ?? new List<MovieResult>();
-
- return results
- .Select(i =>
- {
- var remoteResult = new RemoteSearchResult
- {
- SearchProviderName = TmdbMovieProvider.Current.Name,
- Name = i.Title ?? i.Name ?? i.Original_Title,
- ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(i.Release_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
- {
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
- return remoteResult;
- })
- .ToList();
- }
-
- private async Task<List<RemoteSearchResult>> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("String can't be null or empty.", nameof(name));
- }
-
- string url3;
- if (year == null)
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrl,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- "tv");
- }
- else
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrlTvWithYear,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- year);
- }
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(stream).ConfigureAwait(false);
-
- var results = searchResults.Results ?? new List<TvResult>();
-
- return results
- .Select(i =>
- {
- var remoteResult = new RemoteSearchResult
- {
- SearchProviderName = TmdbMovieProvider.Current.Name,
- Name = i.Name ?? i.Original_Name,
- ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
- {
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
- return remoteResult;
- })
- .ToList();
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
deleted file mode 100644
index c7ba97438..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- internal class TmdbSettingsResult
- {
- public TmdbImageSettings images { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
deleted file mode 100644
index b88ecce87..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Music
-{
- public class TmdbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo>
- {
- public string Name => TmdbMovieProvider.Current.Name;
-
- public Task<MetadataResult<MusicVideo>> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken)
- {
- return TmdbMovieProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken);
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MusicVideoInfo searchInfo, CancellationToken cancellationToken)
- {
- return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index f2d2c8120..3f57c4bc4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -2,40 +2,33 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.People;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
public class TmdbPersonImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IServerConfigurationManager _config;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
+ public TmdbPersonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _config = config;
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
- public static string ProviderName => TmdbUtils.ProviderName;
-
/// <inheritdoc />
- public string Name => ProviderName;
+ public string Name => TmdbUtils.ProviderName;
/// <inheritdoc />
public int Order => 0;
@@ -56,78 +49,37 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var person = (Person)item;
- var id = person.GetProviderId(MetadataProvider.Tmdb);
-
- if (!string.IsNullOrEmpty(id))
- {
- await TmdbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = TmdbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id);
-
- var result = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
-
- var images = result.Images ?? new PersonImages();
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- return GetImages(images, item.GetPreferredMetadataLanguage(), tmdbImageUrl);
- }
-
- return new List<RemoteImageInfo>();
- }
-
- private IEnumerable<RemoteImageInfo> GetImages(PersonImages images, string preferredLanguage, string baseImageUrl)
- {
- var list = new List<RemoteImageInfo>();
+ var personTmdbId = Convert.ToInt32(person.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (images.Profiles != null)
+ if (personTmdbId > 0)
{
- list.AddRange(images.Profiles.Select(i => new RemoteImageInfo
+ var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
+ if (personResult?.Images?.Profiles == null)
{
- ProviderName = Name,
- Type = ImageType.Primary,
- Width = i.Width,
- Height = i.Height,
- Language = GetLanguage(i),
- Url = baseImageUrl + i.File_Path
- }));
- }
-
- var language = preferredLanguage;
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
- {
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
+ return Enumerable.Empty<RemoteImageInfo>();
}
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
+ var remoteImages = new List<RemoteImageInfo>();
+ var language = item.GetPreferredMetadataLanguage();
- if (string.IsNullOrEmpty(i.Language))
+ for (var i = 0; i < personResult.Images.Profiles.Count; i++)
{
- return isLanguageEn ? 3 : 2;
+ var image = personResult.Images.Profiles[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
+ });
}
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
+ return remoteImages.OrderByLanguageDescending(language);
+ }
- private string GetLanguage(Profile profile)
- {
- return profile.Iso_639_1?.ToString();
+ return Enumerable.Empty<RemoteImageInfo>();
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 777ebce49..4384c203e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -3,198 +3,130 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
-using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.People;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo>
{
- private const string DataFileName = "info.json";
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbPersonProvider(
- IFileSystem fileSystem,
- IServerConfigurationManager configurationManager,
- IJsonSerializer jsonSerializer,
- IHttpClientFactory httpClientFactory)
+ public TmdbPersonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
- Current = this;
+ _tmdbClientManager = tmdbClientManager;
}
- internal static TmdbPersonProvider Current { get; private set; }
-
public string Name => TmdbUtils.ProviderName;
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
{
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+ var personTmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!string.IsNullOrEmpty(tmdbId))
+ if (personTmdbId <= 0)
{
- await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false);
+ var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
- var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
- var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
-
- IReadOnlyList<Profile> images = info.Images?.Profiles ?? Array.Empty<Profile>();
-
- var result = new RemoteSearchResult
+ if (personResult != null)
{
- Name = info.Name,
-
- SearchProviderName = Name,
+ var result = new RemoteSearchResult
+ {
+ Name = personResult.Name,
+ SearchProviderName = Name,
+ Overview = personResult.Biography
+ };
- ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
- };
+ if (personResult.Images?.Profiles != null && personResult.Images.Profiles.Count > 0)
+ {
+ result.ImageUrl = _tmdbClientManager.GetProfileUrl(personResult.Images.Profiles[0].FilePath);
+ }
- result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
- result.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id);
+ result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
+ result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
- return new[] { result };
+ return new[] { result };
+ }
}
+ // TODO why? Because of the old rate limit?
if (searchInfo.IsAutomated)
{
// Don't hammer moviedb searching by name
- return Array.Empty<RemoteSearchResult>();
+ return Enumerable.Empty<RemoteSearchResult>();
}
- var url = string.Format(
- CultureInfo.InvariantCulture,
- TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}",
- WebUtility.UrlEncode(searchInfo.Name),
- TmdbUtils.ApiKey);
+ var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
+ var remoteSearchResults = new List<RemoteSearchResult>();
+ for (var i = 0; i < personSearchResult.Count; i++)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
-
- var result2 = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(stream).ConfigureAwait(false)
- ?? new TmdbSearchResult<PersonSearchResult>();
-
- return result2.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
- }
-
- private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl)
- {
- var result = new RemoteSearchResult
- {
- SearchProviderName = Name,
-
- Name = i.Name,
-
- ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : baseImageUrl + i.Profile_Path
- };
+ var person = personSearchResult[i];
+ var remoteSearchResult = new RemoteSearchResult
+ {
+ SearchProviderName = Name,
+ Name = person.Name,
+ ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath)
+ };
- result.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+ remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
+ remoteSearchResults.Add(remoteSearchResult);
+ }
- return result;
+ return remoteSearchResults;
}
public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken)
{
- var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
+ var personTmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
// We don't already have an Id, need to fetch it
- if (string.IsNullOrEmpty(tmdbId))
+ if (personTmdbId <= 0)
{
- tmdbId = await GetTmdbId(id, cancellationToken).ConfigureAwait(false);
+ var personSearchResults = await _tmdbClientManager.SearchPersonAsync(id.Name, cancellationToken).ConfigureAwait(false);
+ if (personSearchResults.Count > 0)
+ {
+ personTmdbId = personSearchResults[0].Id;
+ }
}
var result = new MetadataResult<Person>();
- if (!string.IsNullOrEmpty(tmdbId))
+ if (personTmdbId > 0)
{
- try
- {
- await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false);
- }
- catch (HttpException ex)
- {
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
- {
- return result;
- }
-
- throw;
- }
-
- var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
-
- var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
+ var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
- var item = new Person();
result.HasMetadata = true;
- // Take name from incoming info, don't rename the person
- // TODO: This should go in PersonMetadataService, not each person provider
- item.Name = id.Name;
-
- // item.HomePageUrl = info.homepage;
-
- if (!string.IsNullOrWhiteSpace(info.Place_Of_Birth))
+ var item = new Person
{
- item.ProductionLocations = new string[] { info.Place_Of_Birth };
- }
-
- item.Overview = info.Biography;
-
- if (DateTime.TryParseExact(info.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date))
- {
- item.PremiereDate = date.ToUniversalTime();
- }
+ // Take name from incoming info, don't rename the person
+ // TODO: This should go in PersonMetadataService, not each person provider
+ Name = id.Name,
+ HomePageUrl = person.Homepage,
+ Overview = person.Biography,
+ PremiereDate = person.Birthday?.ToUniversalTime(),
+ EndDate = person.Deathday?.ToUniversalTime()
+ };
- if (DateTime.TryParseExact(info.Deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
+ if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
{
- item.EndDate = date.ToUniversalTime();
+ item.ProductionLocations = new[] { person.PlaceOfBirth };
}
- item.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
+ item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
- if (!string.IsNullOrEmpty(info.Imdb_Id))
+ if (!string.IsNullOrEmpty(person.ImdbId))
{
- item.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id);
+ item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
}
result.HasMetadata = true;
@@ -204,65 +136,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return result;
}
- /// <summary>
- /// Gets the TMDB id.
- /// </summary>
- /// <param name="info">The information.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{System.String}.</returns>
- private async Task<string> GetTmdbId(PersonLookupInfo info, CancellationToken cancellationToken)
- {
- var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
-
- return results.Select(i => i.GetProviderId(MetadataProvider.Tmdb)).FirstOrDefault();
- }
-
- internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken)
- {
- var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, id);
-
- var fileInfo = _fileSystem.GetFileSystemInfo(dataFilePath);
-
- if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
- {
- return;
- }
-
- var url = string.Format(
- CultureInfo.InvariantCulture,
- TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids",
- TmdbUtils.ApiKey,
- id);
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
- await response.Content.CopyToAsync(fs).ConfigureAwait(false);
- }
-
- private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1);
-
- return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId);
- }
-
- internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
- {
- return Path.Combine(GetPersonDataPath(appPaths, tmdbId), DataFileName);
- }
-
- private static string GetPersonsDataPath(IApplicationPaths appPaths)
- {
- return Path.Combine(appPaths.CachePath, "tmdb-people");
- }
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index c56774f8e..3b7a0b254 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -2,40 +2,37 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
- public class TmdbEpisodeImageProvider :
- TmdbEpisodeProviderBase,
- IRemoteImageProvider,
- IHasOrder
+ public class TmdbEpisodeImageProvider : IRemoteImageProvider, IHasOrder
{
- public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
+ _httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
- public string Name => TmdbUtils.ProviderName;
-
// After TheTvDb
public int Order => 1;
+ public string Name => TmdbUtils.ProviderName;
+
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
@@ -49,13 +46,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var episode = (Controller.Entities.TV.Episode)item;
var series = episode.Series;
- var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
+ var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- var list = new List<RemoteImageInfo>();
-
- if (string.IsNullOrEmpty(seriesId))
+ if (seriesTmdbId <= 0)
{
- return list;
+ return Enumerable.Empty<RemoteImageInfo>();
}
var seasonNumber = episode.ParentIndexNumber;
@@ -63,71 +58,45 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
- return list;
+ return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
- var response = await GetEpisodeInfo(
- seriesId,
- seasonNumber.Value,
- episodeNumber.Value,
- language,
- cancellationToken).ConfigureAwait(false);
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+ var episodeResult = await _tmdbClientManager
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- list.AddRange(GetPosters(response.Images).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- }));
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
+ var stills = episodeResult?.Images?.Stills;
+ if (stills == null)
{
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- if (string.IsNullOrEmpty(i.Language))
+ var remoteImages = new RemoteImageInfo[stills.Count];
+ for (var i = 0; i < stills.Count; i++)
+ {
+ var image = stills[i];
+ remoteImages[i] = new RemoteImageInfo
{
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
+ Url = _tmdbClientManager.GetStillUrl(image.FilePath),
+ CommunityRating = image.VoteAverage,
+ VoteCount = image.VoteCount,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ };
+ }
- private IEnumerable<Still> GetPosters(StillImages images)
- {
- return images.Stills ?? new List<Still>();
+ return remoteImages.OrderByLanguageDescending(language);
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return GetResponse(url, cancellationToken);
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
public bool Supports(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index a7e3a03fe..93998a110 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -4,32 +4,27 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
- public class TmdbEpisodeProvider :
- TmdbEpisodeProviderBase,
- IRemoteMetadataProvider<Episode, EpisodeInfo>,
- IHasOrder
+ public class TmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
- public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
+ _httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
// After TheTvDb
@@ -39,21 +34,24 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
- var list = new List<RemoteSearchResult>();
-
// The search query must either provide an episode number or date
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
{
- return list;
+ return Enumerable.Empty<RemoteSearchResult>();
}
var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
- if (metadataResult.HasMetadata)
+ if (!metadataResult.HasMetadata)
{
- var item = metadataResult.Item;
+ return Enumerable.Empty<RemoteSearchResult>();
+ }
- list.Add(new RemoteSearchResult
+ var item = metadataResult.Item;
+
+ return new[]
+ {
+ new RemoteSearchResult
{
IndexNumber = item.IndexNumber,
Name = item.Name,
@@ -63,27 +61,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
ProviderIds = item.ProviderIds,
SearchProviderName = Name,
IndexNumberEnd = item.IndexNumberEnd
- });
- }
-
- return list;
+ }
+ };
}
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Episode>();
+ var metadataResult = new MetadataResult<Episode>();
// Allowing this will dramatically increase scan times
if (info.IsMissingEpisode)
{
- return result;
+ return metadataResult;
}
- info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId);
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string tmdbId);
- if (string.IsNullOrEmpty(seriesTmdbId))
+ var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture);
+ if (seriesTmdbId <= 0)
{
- return result;
+ return metadataResult;
}
var seasonNumber = info.ParentIndexNumber;
@@ -91,125 +88,120 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
- return result;
+ return metadataResult;
}
- try
- {
- var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ var episodeResult = await _tmdbClientManager
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- result.HasMetadata = true;
- result.QueriedById = true;
+ if (episodeResult == null)
+ {
+ return metadataResult;
+ }
- if (!string.IsNullOrEmpty(response.Overview))
- {
- // if overview is non-empty, we can assume that localized data was returned
- result.ResultLanguage = info.MetadataLanguage;
- }
+ metadataResult.HasMetadata = true;
+ metadataResult.QueriedById = true;
- var item = new Episode();
- result.Item = item;
+ if (!string.IsNullOrEmpty(episodeResult.Overview))
+ {
+ // if overview is non-empty, we can assume that localized data was returned
+ metadataResult.ResultLanguage = info.MetadataLanguage;
+ }
- item.Name = info.Name;
- item.IndexNumber = info.IndexNumber;
- item.ParentIndexNumber = info.ParentIndexNumber;
- item.IndexNumberEnd = info.IndexNumberEnd;
+ var item = new Episode
+ {
+ Name = info.Name,
+ IndexNumber = info.IndexNumber,
+ ParentIndexNumber = info.ParentIndexNumber,
+ IndexNumberEnd = info.IndexNumberEnd
+ };
- if (response.External_Ids != null && response.External_Ids.Tvdb_Id > 0)
- {
- item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
- }
+ if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId);
+ }
- item.PremiereDate = response.Air_Date;
- item.ProductionYear = result.Item.PremiereDate.Value.Year;
+ item.PremiereDate = episodeResult.AirDate;
+ item.ProductionYear = episodeResult.AirDate?.Year;
- item.Name = response.Name;
- item.Overview = response.Overview;
+ item.Name = episodeResult.Name;
+ item.Overview = episodeResult.Overview;
- item.CommunityRating = (float)response.Vote_Average;
+ item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage);
- if (response.Videos?.Results != null)
+ if (episodeResult.Videos?.Results != null)
+ {
+ foreach (var video in episodeResult.Videos.Results)
{
- foreach (var video in response.Videos.Results)
+ if (TmdbUtils.IsTrailerType(video))
{
- if (video.Type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase)
- || video.Type.Equals("clip", System.StringComparison.OrdinalIgnoreCase))
- {
- if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase))
- {
- var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key);
- item.AddTrailerUrl(videoUrl);
- }
- }
+ item.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key);
}
}
+ }
- result.ResetPeople();
+ var credits = episodeResult.Credits;
- var credits = response.Credits;
- if (credits != null)
+ if (credits?.Cast != null)
+ {
+ foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
{
- // Actors, Directors, Writers - all in People
- // actors come from cast
- if (credits.Cast != null)
- {
- foreach (var actor in credits.Cast.OrderBy(a => a.Order))
- {
- result.AddPerson(new PersonInfo { Name = actor.Name.Trim(), Role = actor.Character, Type = PersonType.Actor, SortOrder = actor.Order });
- }
- }
-
- // guest stars
- if (credits.Guest_Stars != null)
+ metadataResult.AddPerson(new PersonInfo
{
- foreach (var guest in credits.Guest_Stars.OrderBy(a => a.Order))
- {
- result.AddPerson(new PersonInfo { Name = guest.Name.Trim(), Role = guest.Character, Type = PersonType.GuestStar, SortOrder = guest.Order });
- }
- }
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ });
+ }
+ }
- // and the rest from crew
- if (credits.Crew != null)
+ if (credits?.GuestStars != null)
+ {
+ foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ {
+ metadataResult.AddPerson(new PersonInfo
{
- var keepTypes = new[]
- {
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
- };
-
- foreach (var person in credits.Crew)
- {
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
-
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
- !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- result.AddPerson(new PersonInfo { Name = person.Name.Trim(), Role = person.Job, Type = type });
- }
- }
+ Name = guest.Name.Trim(),
+ Role = guest.Character,
+ Type = PersonType.GuestStar,
+ SortOrder = guest.Order
+ });
}
}
- catch (HttpException ex)
+
+ // and the rest from crew
+ if (credits?.Crew != null)
{
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ foreach (var person in credits.Crew)
{
- return result;
- }
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- throw;
+ metadataResult.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
}
- return result;
+ metadataResult.Item = item;
+
+ return metadataResult;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return GetResponse(url, cancellationToken);
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
deleted file mode 100644
index 34d2424a3..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.TV
-{
- public abstract class TmdbEpisodeProviderBase
- {
- private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
-
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<TmdbEpisodeProviderBase> _logger;
-
- protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- {
- _httpClientFactory = httpClientFactory;
- _configurationManager = configurationManager;
- _jsonSerializer = jsonSerializer;
- _fileSystem = fileSystem;
- _logger = loggerFactory.CreateLogger<TmdbEpisodeProviderBase>();
- }
-
- protected ILogger Logger => _logger;
-
- protected async Task<EpisodeResult> GetEpisodeInfo(
- string seriesTmdbId,
- int season,
- int episodeNumber,
- string preferredMetadataLanguage,
- CancellationToken cancellationToken)
- {
- await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage);
-
- return _jsonSerializer.DeserializeFromFile<EpisodeResult>(dataFilePath);
- }
-
- internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- if (string.IsNullOrEmpty(language))
- {
- throw new ArgumentNullException(nameof(language));
- }
-
- var path = GetDataFilePath(tmdbId, seasonNumber, episodeNumber, language);
-
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
-
- if (fileInfo.Exists)
- {
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
- {
- return Task.CompletedTask;
- }
- }
-
- return DownloadEpisodeInfo(tmdbId, seasonNumber, episodeNumber, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, int seasonNumber, int episodeNumber, string preferredLanguage)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- if (string.IsNullOrEmpty(preferredLanguage))
- {
- throw new ArgumentNullException(nameof(preferredLanguage));
- }
-
- var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
-
- var filename = string.Format(
- CultureInfo.InvariantCulture,
- "season-{0}-episode-{1}-{2}.json",
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- episodeNumber.ToString(CultureInfo.InvariantCulture),
- preferredLanguage);
-
- return Path.Combine(path, filename);
- }
-
- internal async Task DownloadEpisodeInfo(string id, int seasonNumber, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var mainResult = await FetchMainResult(EpisodeUrlPattern, id, seasonNumber, episodeNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(id, seasonNumber, episodeNumber, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
- }
-
- internal async Task<EpisodeResult> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(
- CultureInfo.InvariantCulture,
- urlPattern,
- id,
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- episodeNumber,
- TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", language);
- }
-
- var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
- // Get images in english and with no language
- url += "&include_image_language=" + includeImageLanguageParam;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(stream).ConfigureAwait(false);
- }
-
- protected Task<HttpResponseMessage> GetResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index dcc7f8700..f4ed480ae 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -13,29 +13,25 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
+ public TmdbSeasonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
public int Order => 1;
- public string Name => ProviderName;
-
- public static string ProviderName => TmdbUtils.ProviderName;
+ public string Name => TmdbUtils.ProviderName;
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
@@ -45,87 +41,46 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var season = (Season)item;
- var series = season.Series;
-
- var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
-
- if (string.IsNullOrEmpty(seriesId))
- {
- return Enumerable.Empty<RemoteImageInfo>();
- }
+ var series = season?.Series;
- var seasonNumber = season.IndexNumber;
+ var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!seasonNumber.HasValue)
+ if (seriesTmdbId <= 0 || season?.IndexNumber == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
- var results = await FetchImages(season, seriesId, language, cancellationToken).ConfigureAwait(false);
+ var seasonResult = await _tmdbClientManager
+ .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var list = results.Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- });
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
+ var posters = seasonResult?.Images?.Posters;
+ if (posters == null)
{
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken)
- {
- var seasonNumber = item.IndexNumber.GetValueOrDefault();
- await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, seasonNumber, language, cancellationToken).ConfigureAwait(false);
-
- var path = TmdbSeasonProvider.Current.GetDataFilePath(tmdbId, seasonNumber, language);
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- if (!string.IsNullOrEmpty(path))
+ var remoteImages = new RemoteImageInfo[posters.Count];
+ for (var i = 0; i < posters.Count; i++)
{
- if (File.Exists(path))
+ var image = posters[i];
+ remoteImages[i] = new RemoteImageInfo
{
- return _jsonSerializer.DeserializeFromFile<Models.TV.SeasonResult>(path).Images.Posters;
- }
+ Url = _tmdbClientManager.GetPosterUrl(image.FilePath),
+ CommunityRating = image.VoteAverage,
+ VoteCount = image.VoteCount,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ };
}
- return null;
+ return remoteImages.OrderByLanguageDescending(language);
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index c9b257fcc..6ca462474 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -3,53 +3,28 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using System.Net;
+using System.Linq;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
-using Season = MediaBrowser.Controller.Entities.TV.Season;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>
{
- private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos";
-
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<TmdbSeasonProvider> _logger;
-
- internal static TmdbSeasonProvider Current { get; private set; }
-
- public TmdbSeasonProvider(
- IHttpClientFactory httpClientFactory,
- IServerConfigurationManager configurationManager,
- IFileSystem fileSystem,
- IJsonSerializer jsonSerializer,
- ILogger<TmdbSeasonProvider> logger)
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
_httpClientFactory = httpClientFactory;
- _configurationManager = configurationManager;
- _fileSystem = fileSystem;
- _jsonSerializer = jsonSerializer;
- _logger = logger;
- Current = this;
+ _tmdbClientManager = tmdbClientManager;
}
public string Name => TmdbUtils.ProviderName;
@@ -62,180 +37,86 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var seasonNumber = info.IndexNumber;
- if (!string.IsNullOrWhiteSpace(seriesTmdbId) && seasonNumber.HasValue)
+ if (string.IsNullOrWhiteSpace(seriesTmdbId) || !seasonNumber.HasValue)
{
- try
- {
- var seasonInfo = await GetSeasonInfo(seriesTmdbId, seasonNumber.Value, info.MetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
-
- result.HasMetadata = true;
- result.Item = new Season();
-
- // Don't use moviedb season names for now until if/when we have field-level configuration
- // result.Item.Name = seasonInfo.name;
-
- result.Item.Name = info.Name;
-
- result.Item.IndexNumber = seasonNumber;
-
- result.Item.Overview = seasonInfo.Overview;
-
- if (seasonInfo.External_Ids != null && seasonInfo.External_Ids.Tvdb_Id > 0)
- {
- result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- var credits = seasonInfo.Credits;
- if (credits != null)
- {
- // Actors, Directors, Writers - all in People
- // actors come from cast
- if (credits.Cast != null)
- {
- // foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order });
- }
-
- // and the rest from crew
- if (credits.Crew != null)
- {
- // foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department });
- }
- }
-
- result.Item.PremiereDate = seasonInfo.Air_Date;
- result.Item.ProductionYear = result.Item.PremiereDate.Value.Year;
- }
- catch (HttpException ex)
- {
- _logger.LogError(ex, "No metadata found for {0}", seasonNumber.Value);
-
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
- {
- return result;
- }
-
- throw;
- }
+ return result;
}
- return result;
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
- {
- return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
-
- private async Task<SeasonResult> GetSeasonInfo(
- string seriesTmdbId,
- int season,
- string preferredMetadataLanguage,
- CancellationToken cancellationToken)
- {
- await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage);
-
- return _jsonSerializer.DeserializeFromFile<SeasonResult>(dataFilePath);
- }
+ var seasonResult = await _tmdbClientManager
+ .GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
+ if (seasonResult == null)
{
- throw new ArgumentNullException(nameof(tmdbId));
+ return result;
}
- if (string.IsNullOrEmpty(language))
+ result.HasMetadata = true;
+ result.Item = new Season
{
- throw new ArgumentNullException(nameof(language));
- }
+ Name = info.Name,
+ IndexNumber = seasonNumber,
+ Overview = seasonResult?.Overview
+ };
- var path = GetDataFilePath(tmdbId, seasonNumber, language);
-
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
+ if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
+ {
+ result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
+ }
- if (fileInfo.Exists)
+ // TODO why was this disabled?
+ var credits = seasonResult.Credits;
+ if (credits?.Cast != null)
{
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList();
+ for (var i = 0; i < cast.Count; i++)
{
- return Task.CompletedTask;
+ result.AddPerson(new PersonInfo
+ {
+ Name = cast[i].Name.Trim(),
+ Role = cast[i].Character,
+ Type = PersonType.Actor,
+ SortOrder = cast[i].Order
+ });
}
}
- return DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, int seasonNumber, string preferredLanguage)
- {
- if (string.IsNullOrEmpty(tmdbId))
+ if (credits?.Crew != null)
{
- throw new ArgumentNullException(nameof(tmdbId));
- }
+ foreach (var person in credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
- if (string.IsNullOrEmpty(preferredLanguage))
- {
- throw new ArgumentNullException(nameof(preferredLanguage));
- }
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
+ result.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
+ }
- var filename = string.Format(
- CultureInfo.InvariantCulture,
- "season-{0}-{1}.json",
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- preferredLanguage);
+ result.Item.PremiereDate = seasonResult.AirDate;
+ result.Item.ProductionYear = seasonResult.AirDate?.Year;
- return Path.Combine(path, filename);
+ return result;
}
- internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
{
- var mainResult = await FetchMainResult(id, seasonNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(id, seasonNumber, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ return Task.FromResult(Enumerable.Empty<RemoteSearchResult>());
}
- internal async Task<SeasonResult> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- var url = string.Format(
- CultureInfo.InvariantCulture,
- GetTvInfo3,
- id,
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
- }
-
- var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
- // Get images in english and with no language
- url += "&include_image_language=" + includeImageLanguageParam;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(stream).ConfigureAwait(false);
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 179ceb825..d0c6b8b88 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -13,28 +13,23 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
+ public TmdbSeriesImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
- public string Name => ProviderName;
-
- public static string ProviderName => TmdbUtils.ProviderName;
+ public string Name => TmdbUtils.ProviderName;
// After tvdb and fanart
public int Order => 2;
@@ -55,124 +50,63 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
- var list = new List<RemoteImageInfo>();
-
- var results = await FetchImages(item, null, cancellationToken).ConfigureAwait(false);
+ var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
- if (results == null)
+ if (string.IsNullOrEmpty(tmdbId))
{
- return list;
+ return null;
}
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
var language = item.GetPreferredMetadataLanguage();
- list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- }));
-
- list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + i.File_Path,
- CommunityRating = i.Vote_Average,
- VoteCount = i.Vote_Count,
- Width = i.Width,
- Height = i.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- }));
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
- {
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- /// <summary>
- /// Gets the posters.
- /// </summary>
- /// <param name="images">The images.</param>
- private IEnumerable<Poster> GetPosters(Images images)
- {
- return images.Posters ?? new List<Poster>();
- }
+ var series = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- /// <summary>
- /// Gets the backdrops.
- /// </summary>
- /// <param name="images">The images.</param>
- private IEnumerable<Backdrop> GetBackdrops(Images images)
- {
- var eligibleBackdrops = images.Backdrops ?? new List<Backdrop>();
+ if (series?.Images == null)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
- .ThenByDescending(i => i.Vote_Count);
- }
+ var posters = series.Images.Posters;
+ var backdrops = series.Images.Backdrops;
- /// <summary>
- /// Fetches the images.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="language">The language.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{MovieImages}.</returns>
- private async Task<Images> FetchImages(
- BaseItem item,
- string language,
- CancellationToken cancellationToken)
- {
- var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
+ var remoteImages = new RemoteImageInfo[posters.Count + backdrops.Count];
- if (string.IsNullOrEmpty(tmdbId))
+ for (var i = 0; i < posters.Count; i++)
{
- return null;
+ var poster = posters[i];
+ remoteImages[i] = new RemoteImageInfo
+ {
+ Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
+ CommunityRating = poster.VoteAverage,
+ VoteCount = poster.VoteCount,
+ Width = poster.Width,
+ Height = poster.Height,
+ Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ };
}
- await TmdbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
-
- if (!string.IsNullOrEmpty(path) && File.Exists(path))
+ for (var i = 0; i < backdrops.Count; i++)
{
- return _jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
+ var backdrop = series.Images.Backdrops[i];
+ remoteImages[posters.Count + i] = new RemoteImageInfo
+ {
+ Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
+ CommunityRating = backdrop.VoteAverage,
+ VoteCount = backdrop.VoteCount,
+ Width = backdrop.Width,
+ Height = backdrop.Height,
+ ProviderName = Name,
+ Type = ImageType.Backdrop,
+ RatingType = RatingType.Score
+ };
}
- return null;
+ return remoteImages.OrderByLanguageDescending(language);
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 287ebca8c..942c85b90 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -3,107 +3,81 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
+using TMDbLib.Objects.Find;
+using TMDbLib.Objects.Search;
+using TMDbLib.Objects.TvShows;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
{
- private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
-
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly ILogger<TmdbSeriesProvider> _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILibraryManager _libraryManager;
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private readonly TmdbClientManager _tmdbClientManager;
public TmdbSeriesProvider(
- IJsonSerializer jsonSerializer,
- IServerConfigurationManager configurationManager,
- ILogger<TmdbSeriesProvider> logger,
IHttpClientFactory httpClientFactory,
- ILibraryManager libraryManager)
+ TmdbClientManager tmdbClientManager)
{
- _jsonSerializer = jsonSerializer;
- _configurationManager = configurationManager;
- _logger = logger;
_httpClientFactory = httpClientFactory;
- _libraryManager = libraryManager;
+ _tmdbClientManager = tmdbClientManager;
Current = this;
}
- internal static TmdbSeriesProvider Current { get; private set; }
-
public string Name => TmdbUtils.ProviderName;
// After TheTVDB
public int Order => 1;
+ internal static TmdbSeriesProvider Current { get; private set; }
+
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
if (!string.IsNullOrEmpty(tmdbId))
{
- cancellationToken.ThrowIfCancellationRequested();
-
- await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
-
- var obj = _jsonSerializer.DeserializeFromFile<SeriesResult>(dataFilePath);
+ var series = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var remoteResult = new RemoteSearchResult
+ if (series != null)
{
- Name = obj.Name,
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
- };
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
- remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id);
+ var remoteResult = MapTvShowToRemoteSearchResult(series);
- if (obj.External_Ids != null && obj.External_Ids.Tvdb_Id > 0)
- {
- remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture));
+ return new[] { remoteResult };
}
-
- return new[] { remoteResult };
}
var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrEmpty(imdbId))
{
- var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
+ var findResult = await _tmdbClientManager
+ .FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
- if (searchResult != null)
+ var tvResults = findResult?.TvResults;
+ if (tvResults != null)
{
- return new[] { searchResult };
+ var imdbIdResults = new RemoteSearchResult[tvResults.Count];
+ for (var i = 0; i < tvResults.Count; i++)
+ {
+ var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]);
+ remoteResult.SetProviderId(MetadataProvider.Imdb, imdbId);
+ imdbIdResults[i] = remoteResult;
+ }
+
+ return imdbIdResults;
}
}
@@ -111,15 +85,80 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(tvdbId))
{
- var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
+ var findResult = await _tmdbClientManager
+ .FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+
+ var tvResults = findResult?.TvResults;
+ if (tvResults != null)
+ {
+ var tvIdResults = new RemoteSearchResult[tvResults.Count];
+ for (var i = 0; i < tvResults.Count; i++)
+ {
+ var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]);
+ remoteResult.SetProviderId(MetadataProvider.Tvdb, tvdbId);
+ tvIdResults[i] = remoteResult;
+ }
+
+ return tvIdResults;
+ }
+ }
+
+ var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+
+ var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
+ for (var i = 0; i < tvSearchResults.Count; i++)
+ {
+ remoteResults[i] = MapSearchTvToRemoteSearchResult(tvSearchResults[i]);
+ }
+
+ return remoteResults;
+ }
+
+ private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series)
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = series.Name ?? series.OriginalName,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
+ Overview = series.Overview
+ };
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
+ if (series.ExternalIds != null)
+ {
+ if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId))
+ {
+ remoteResult.SetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
+ }
- if (searchResult != null)
+ if (!string.IsNullOrEmpty(series.ExternalIds.TvdbId))
{
- return new[] { searchResult };
+ remoteResult.SetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
}
}
- return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
+ remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+
+ return remoteResult;
+ }
+
+ private RemoteSearchResult MapSearchTvToRemoteSearchResult(SearchTv series)
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = series.Name ?? series.OriginalName,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
+ Overview = series.Overview
+ };
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
+ remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+
+ return remoteResult;
}
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
@@ -137,11 +176,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(imdbId))
{
- var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
+ var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult != null)
{
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
}
}
@@ -152,11 +191,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(tvdbId))
{
- var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
+ var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult != null)
{
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
}
}
@@ -164,13 +203,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId))
{
result.QueriedById = false;
- var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false);
-
- var searchResult = searchResults.FirstOrDefault();
+ var searchResults = await _tmdbClientManager.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
- if (searchResult != null)
+ if (searchResults.Count > 0)
{
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
@@ -178,7 +215,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
cancellationToken.ThrowIfCancellationRequested();
- result = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ var tvShow = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
+
+ result = new MetadataResult<Series>
+ {
+ Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
+ ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
+ };
+
+ foreach (var person in GetPersons(tvShow))
+ {
+ result.AddPerson(person);
+ }
result.HasMetadata = result.Item != null;
}
@@ -186,99 +236,74 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return result;
}
- private async Task<MetadataResult<Series>> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
+ private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
{
- SeriesResult seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- if (seriesInfo == null)
+ var series = new Series
{
- return null;
- }
-
- tmdbId = seriesInfo.Id.ToString(_usCulture);
-
- string dataFilePath = GetDataFilePath(tmdbId, language);
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath);
-
- await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- var result = new MetadataResult<Series>
- {
- Item = new Series(),
- ResultLanguage = seriesInfo.ResultLanguage
+ Name = seriesResult.Name,
+ OriginalTitle = seriesResult.OriginalName
};
- var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- ProcessMainInfo(result, seriesInfo, preferredCountryCode, settings);
-
- return result;
- }
-
- private void ProcessMainInfo(MetadataResult<Series> seriesResult, SeriesResult seriesInfo, string preferredCountryCode, TmdbSettingsResult settings)
- {
- var series = seriesResult.Item;
+ series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture));
- series.Name = seriesInfo.Name;
- series.OriginalTitle = seriesInfo.Original_Name;
- series.SetProviderId(MetadataProvider.Tmdb, seriesInfo.Id.ToString(_usCulture));
+ series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage);
- string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
+ series.Overview = seriesResult.Overview;
- if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
+ if (seriesResult.Networks != null)
{
- series.CommunityRating = rating;
+ series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray();
}
- series.Overview = seriesInfo.Overview;
-
- if (seriesInfo.Networks != null)
+ if (seriesResult.Genres != null)
{
- series.Studios = seriesInfo.Networks.Select(i => i.Name).ToArray();
+ series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray();
}
- if (seriesInfo.Genres != null)
+ if (seriesResult.Keywords?.Results != null)
{
- series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
+ for (var i = 0; i < seriesResult.Keywords.Results.Count; i++)
+ {
+ series.AddTag(seriesResult.Keywords.Results[i].Name);
+ }
}
- series.HomePageUrl = seriesInfo.Homepage;
+ series.HomePageUrl = seriesResult.Homepage;
- series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
+ series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
- if (string.Equals(seriesInfo.Status, "Ended", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase))
{
series.Status = SeriesStatus.Ended;
- series.EndDate = seriesInfo.Last_Air_Date;
+ series.EndDate = seriesResult.LastAirDate;
}
else
{
series.Status = SeriesStatus.Continuing;
}
- series.PremiereDate = seriesInfo.First_Air_Date;
+ series.PremiereDate = seriesResult.FirstAirDate;
- var ids = seriesInfo.External_Ids;
+ var ids = seriesResult.ExternalIds;
if (ids != null)
{
- if (!string.IsNullOrWhiteSpace(ids.Imdb_Id))
+ if (!string.IsNullOrWhiteSpace(ids.ImdbId))
{
- series.SetProviderId(MetadataProvider.Imdb, ids.Imdb_Id);
+ series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId);
}
- if (ids.Tvrage_Id > 0)
+ if (!string.IsNullOrEmpty(ids.TvrageId))
{
- series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.Value.ToString(_usCulture));
+ series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId);
}
- if (ids.Tvdb_Id > 0)
+ if (!string.IsNullOrEmpty(ids.TvdbId))
{
- series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.ToString(_usCulture));
+ series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
}
}
- var contentRatings = (seriesInfo.Content_Ratings ?? new ContentRatings()).Results ?? new List<ContentRating>();
+ var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
@@ -297,254 +322,72 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.OfficialRating = minimumRelease.Rating;
}
- if (seriesInfo.Videos != null && seriesInfo.Videos.Results != null)
+ if (seriesResult.Videos?.Results != null)
{
- foreach (var video in seriesInfo.Videos.Results)
+ foreach (var video in seriesResult.Videos.Results)
{
- if ((video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
- || video.Type.Equals("clip", StringComparison.OrdinalIgnoreCase))
- && video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase))
+ if (TmdbUtils.IsTrailerType(video))
{
- series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}");
+ series.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key);
}
}
}
- seriesResult.ResetPeople();
- var tmdbImageUrl = settings.images.GetImageUrl("original");
+ return series;
+ }
- if (seriesInfo.Credits != null)
+ private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult)
+ {
+ if (seriesResult.Credits?.Cast != null)
{
- if (seriesInfo.Credits.Cast != null)
+ foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
{
- foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
+ var personInfo = new PersonInfo
{
- var personInfo = new PersonInfo
- {
- Name = actor.Name.Trim(),
- Role = actor.Character,
- Type = PersonType.Actor,
- SortOrder = actor.Order
- };
-
- if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
- }
-
- if (actor.Id > 0)
- {
- personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
- }
-
- seriesResult.AddPerson(personInfo);
- }
- }
-
- if (seriesInfo.Credits.Crew != null)
- {
- var keepTypes = new[]
- {
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
};
- foreach (var person in seriesInfo.Credits.Crew)
+ if (actor.Id > 0)
{
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
-
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- seriesResult.AddPerson(new PersonInfo
- {
- Name = person.Name.Trim(),
- Role = person.Job,
- Type = type
- });
+ personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
}
- }
- }
- }
-
- internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var dataPath = GetSeriesDataPath(appPaths);
-
- return Path.Combine(dataPath, tmdbId);
- }
-
- internal static string GetSeriesDataPath(IApplicationPaths appPaths)
- {
- var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv");
-
- return dataPath;
- }
-
- internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- SeriesResult mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (mainResult == null)
- {
- return;
- }
-
- var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
- }
-
- internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += "&language=" + TmdbMovieProvider.NormalizeLanguage(language)
- + "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage, cancellationToken).ConfigureAwait(false);
- await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).ConfigureAwait(false);
-
- if (!string.IsNullOrEmpty(language))
- {
- mainResult.ResultLanguage = language;
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- // If the language preference isn't english, then have the overview fallback to english if it's blank
- if (mainResult != null &&
- string.IsNullOrEmpty(mainResult.Overview) &&
- !string.IsNullOrEmpty(language) &&
- !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language);
-
- url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
-
- if (!string.IsNullOrEmpty(language))
- {
- // Get images in english and with no language
- url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
- }
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ yield return personInfo;
}
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).ConfigureAwait(false);
-
- mainResult.Overview = englishResult.Overview;
- mainResult.ResultLanguage = "en";
- }
-
- return mainResult;
- }
-
- internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
}
- var path = GetDataFilePath(tmdbId, language);
-
- var fileInfo = new FileInfo(path);
- if (fileInfo.Exists)
+ if (seriesResult.Credits?.Crew != null)
{
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays <= 2)
+ var keepTypes = new[]
{
- return Task.CompletedTask;
- }
- }
-
- return DownloadSeriesInfo(tmdbId, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, string preferredLanguage)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
-
- var filename = string.Format(CultureInfo.InvariantCulture, "series-{0}.json", preferredLanguage ?? string.Empty);
-
- return Path.Combine(path, filename);
- }
-
- private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken)
- {
- var url = string.Format(
- CultureInfo.InvariantCulture,
- TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
- id,
- TmdbUtils.ApiKey,
- externalSource);
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
-
- var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).ConfigureAwait(false);
-
- if (result != null && result.Tv_Results != null)
- {
- var tv = result.Tv_Results.FirstOrDefault();
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
- if (tv != null)
+ foreach (var person in seriesResult.Credits.Crew)
{
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
- var remoteResult = new RemoteSearchResult
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
- Name = tv.Name,
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path)
- ? null
- : tmdbImageUrl + tv.Poster_Path
- };
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
+ continue;
+ }
- return remoteResult;
+ yield return new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ };
}
}
-
- return null;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
new file mode 100644
index 000000000..2dc5cd55d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -0,0 +1,469 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
+using TMDbLib.Client;
+using TMDbLib.Objects.Collections;
+using TMDbLib.Objects.Find;
+using TMDbLib.Objects.General;
+using TMDbLib.Objects.Movies;
+using TMDbLib.Objects.People;
+using TMDbLib.Objects.Search;
+using TMDbLib.Objects.TvShows;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Manager class for abstracting the TMDb API client library.
+ /// </summary>
+ public class TmdbClientManager
+ {
+ private const int CacheDurationInHours = 1;
+
+ private readonly IMemoryCache _memoryCache;
+ private readonly TMDbClient _tmDbClient;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TmdbClientManager"/> class.
+ /// </summary>
+ /// <param name="memoryCache">An instance of <see cref="IMemoryCache"/>.</param>
+ public TmdbClientManager(IMemoryCache memoryCache)
+ {
+ _memoryCache = memoryCache;
+ _tmDbClient = new TMDbClient(TmdbUtils.ApiKey);
+ // Not really interested in NotFoundException
+ _tmDbClient.ThrowApiExceptions = false;
+ }
+
+ /// <summary>
+ /// Gets a movie from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="tmdbId">The movie's TMDb id.</param>
+ /// <param name="language">The movie's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb movie or null if not found.</returns>
+ public async Task<Movie> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out Movie movie))
+ {
+ return movie;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ movie = await _tmDbClient.GetMovieAsync(
+ tmdbId,
+ TmdbUtils.NormalizeLanguage(language),
+ imageLanguages,
+ MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos,
+ cancellationToken).ConfigureAwait(false);
+
+ if (movie != null)
+ {
+ _memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return movie;
+ }
+
+ /// <summary>
+ /// Gets a collection from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="tmdbId">The collection's TMDb id.</param>
+ /// <param name="language">The collection's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb collection or null if not found.</returns>
+ public async Task<Collection> GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out Collection collection))
+ {
+ return collection;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ collection = await _tmDbClient.GetCollectionAsync(
+ tmdbId,
+ TmdbUtils.NormalizeLanguage(language),
+ imageLanguages,
+ CollectionMethods.Images,
+ cancellationToken).ConfigureAwait(false);
+
+ if (collection != null)
+ {
+ _memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return collection;
+ }
+
+ /// <summary>
+ /// Gets a tv show from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="tmdbId">The tv show's TMDb id.</param>
+ /// <param name="language">The tv show's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv show information or null if not found.</returns>
+ public async Task<TvShow> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out TvShow series))
+ {
+ return series;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ series = await _tmDbClient.GetTvShowAsync(
+ tmdbId,
+ language: TmdbUtils.NormalizeLanguage(language),
+ includeImageLanguage: imageLanguages,
+ extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (series != null)
+ {
+ _memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return series;
+ }
+
+ /// <summary>
+ /// Gets a tv season from the TMDb API based on the tv show's TMDb id.
+ /// </summary>
+ /// <param name="tvShowId">The tv season's TMDb id.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="language">The tv season's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv season information or null if not found.</returns>
+ public async Task<TvSeason> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out TvSeason season))
+ {
+ return season;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ season = await _tmDbClient.GetTvSeasonAsync(
+ tvShowId,
+ seasonNumber,
+ language: TmdbUtils.NormalizeLanguage(language),
+ includeImageLanguage: imageLanguages,
+ extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (season != null)
+ {
+ _memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return season;
+ }
+
+ /// <summary>
+ /// Gets a movie from the TMDb API based on the tv show's TMDb id.
+ /// </summary>
+ /// <param name="tvShowId">The tv show's TMDb id.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="episodeNumber">The episode number.</param>
+ /// <param name="language">The episode's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv episode information or null if not found.</returns>
+ public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out TvEpisode episode))
+ {
+ return episode;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ episode = await _tmDbClient.GetTvEpisodeAsync(
+ tvShowId,
+ seasonNumber,
+ episodeNumber,
+ language: TmdbUtils.NormalizeLanguage(language),
+ includeImageLanguage: imageLanguages,
+ extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (episode != null)
+ {
+ _memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return episode;
+ }
+
+ /// <summary>
+ /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="personTmdbId">The person's TMDb id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb person information or null if not found.</returns>
+ public async Task<Person> GetPersonAsync(int personTmdbId, CancellationToken cancellationToken)
+ {
+ var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}";
+ if (_memoryCache.TryGetValue(key, out Person person))
+ {
+ return person;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ person = await _tmDbClient.GetPersonAsync(
+ personTmdbId,
+ PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
+ cancellationToken).ConfigureAwait(false);
+
+ if (person != null)
+ {
+ _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return person;
+ }
+
+ /// <summary>
+ /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id.
+ /// </summary>
+ /// <param name="externalId">The item's external id.</param>
+ /// <param name="source">The source of the id eg. IMDb.</param>
+ /// <param name="language">The item's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb item or null if not found.</returns>
+ public async Task<FindContainer> FindByExternalIdAsync(
+ string externalId,
+ FindExternalSource source,
+ string language,
+ CancellationToken cancellationToken)
+ {
+ var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out FindContainer result))
+ {
+ return result;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ result = await _tmDbClient.FindAsync(
+ source,
+ externalId,
+ TmdbUtils.NormalizeLanguage(language),
+ cancellationToken).ConfigureAwait(false);
+
+ if (result != null)
+ {
+ _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Searches for a tv show using the TMDb API based on its name.
+ /// </summary>
+ /// <param name="name">The name of the tv show.</param>
+ /// <param name="language">The tv show's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv show information.</returns>
+ public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken)
+ {
+ var key = $"searchseries-{name}-{language}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
+ {
+ return series.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Searches for a person based on their name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the person.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb person information.</returns>
+ public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
+ {
+ var key = $"searchperson-{name}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person))
+ {
+ return person.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchPersonAsync(name, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Searches for a movie based on its name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the movie.</param>
+ /// <param name="language">The movie's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb movie information.</returns>
+ public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
+ {
+ return SearchMovieAsync(name, 0, language, cancellationToken);
+ }
+
+ /// <summary>
+ /// Searches for a movie based on its name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the movie.</param>
+ /// <param name="year">The release year of the movie.</param>
+ /// <param name="language">The movie's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb movie information.</returns>
+ public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
+ {
+ var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies))
+ {
+ return movies.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Searches for a collection based on its name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the collection.</param>
+ /// <param name="language">The collection's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb collection information.</returns>
+ public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
+ {
+ var key = $"collectionsearch-{name}-{language}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections))
+ {
+ return collections.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the poster.
+ /// </summary>
+ /// <param name="posterPath">The relative URL of the poster.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetPosterUrl(string posterPath)
+ {
+ if (string.IsNullOrEmpty(posterPath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the backdrop image.
+ /// </summary>
+ /// <param name="posterPath">The relative URL of the backdrop image.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetBackdropUrl(string posterPath)
+ {
+ if (string.IsNullOrEmpty(posterPath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the profile image.
+ /// </summary>
+ /// <param name="actorProfilePath">The relative URL of the profile image.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetProfileUrl(string actorProfilePath)
+ {
+ if (string.IsNullOrEmpty(actorProfilePath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the still image.
+ /// </summary>
+ /// <param name="filePath">The relative URL of the still image.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetStillUrl(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
+ }
+
+ private Task EnsureClientConfigAsync()
+ {
+ return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index 1415d6976..b754a0795 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -1,7 +1,9 @@
+#nullable enable
+
using System;
-using System.Net.Mime;
+using System.Collections.Generic;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using TMDbLib.Objects.General;
namespace MediaBrowser.Providers.Plugins.Tmdb
{
@@ -16,11 +18,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public const string BaseTmdbUrl = "https://www.themoviedb.org/";
/// <summary>
- /// URL of the TMDB API instance to use.
- /// </summary>
- public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
-
- /// <summary>
/// Name of the provider.
/// </summary>
public const string ProviderName = "TheMovieDb";
@@ -31,9 +28,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
/// <summary>
- /// Value of the Accept header for requests to the provider.
+ /// Maximum number of cast members to pull.
+ /// </summary>
+ public const int MaxCastMembers = 15;
+
+ /// <summary>
+ /// The crew types to keep.
/// </summary>
- public static readonly string[] AcceptHeaders = { MediaTypeNames.Application.Json, "image/*" };
+ public static readonly string[] WantedCrewTypes =
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
/// <summary>
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
@@ -59,7 +66,98 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
return PersonType.Writer;
}
- return null;
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Determines whether a video is a trailer.
+ /// </summary>
+ /// <param name="video">The TMDb video.</param>
+ /// <returns>A boolean indicating whether the video is a trailer.</returns>
+ public static bool IsTrailerType(Video video)
+ {
+ return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)
+ && (!video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
+ || !video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
+ }
+
+ /// <summary>
+ /// Normalizes a language string for use with TMDb's include image language parameter.
+ /// </summary>
+ /// <param name="preferredLanguage">The preferred language as either a 2 letter code with or without country code.</param>
+ /// <returns>The comma separated language string.</returns>
+ public static string GetImageLanguagesParam(string preferredLanguage)
+ {
+ var languages = new List<string>();
+
+ if (!string.IsNullOrEmpty(preferredLanguage))
+ {
+ preferredLanguage = NormalizeLanguage(preferredLanguage);
+
+ languages.Add(preferredLanguage);
+
+ if (preferredLanguage.Length == 5) // like en-US
+ {
+ // Currenty, TMDB supports 2-letter language codes only
+ // They are planning to change this in the future, thus we're
+ // supplying both codes if we're having a 5-letter code.
+ languages.Add(preferredLanguage.Substring(0, 2));
+ }
+ }
+
+ languages.Add("null");
+
+ if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ languages.Add("en");
+ }
+
+ return string.Join(',', languages);
+ }
+
+ /// <summary>
+ /// Normalizes a language string for use with TMDb's language parameter.
+ /// </summary>
+ /// <param name="language">The language code.</param>
+ /// <returns>The normalized language code.</returns>
+ public static string NormalizeLanguage(string language)
+ {
+ if (string.IsNullOrEmpty(language))
+ {
+ return language;
+ }
+
+ // They require this to be uppercase
+ // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api.
+ // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
+ var parts = language.Split('-');
+
+ if (parts.Length == 2)
+ {
+ language = parts[0] + "-" + parts[1].ToUpperInvariant();
+ }
+
+ return language;
+ }
+
+ /// <summary>
+ /// Adjusts the image's language code preferring the 5 letter language code eg. en-US.
+ /// </summary>
+ /// <param name="imageLanguage">The image's actual language code.</param>
+ /// <param name="requestLanguage">The requested language code.</param>
+ /// <returns>The language code.</returns>
+ public static string AdjustImageLanguage(string imageLanguage, string requestLanguage)
+ {
+ if (!string.IsNullOrEmpty(imageLanguage)
+ && !string.IsNullOrEmpty(requestLanguage)
+ && requestLanguage.Length > 2
+ && imageLanguage.Length == 2
+ && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
+ {
+ return requestLanguage;
+ }
+
+ return imageLanguage;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
deleted file mode 100644
index 613dc17e3..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
-{
- public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo>
- {
- private readonly IHttpClientFactory _httpClientFactory;
-
- public TmdbTrailerProvider(IHttpClientFactory httpClientFactory)
- {
- _httpClientFactory = httpClientFactory;
- }
-
- public string Name => TmdbMovieProvider.Current.Name;
-
- public int Order => 0;
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
- {
- return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken);
- }
-
- public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
- {
- return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken);
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
- }
-}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index f25d3d5ee..f3fbe2d12 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -150,37 +150,11 @@ namespace MediaBrowser.Providers.Subtitles
var parts = subtitleId.Split(new[] { '_' }, 2);
var provider = GetProvider(parts[0]);
- var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
-
try
{
var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
- using (var stream = response.Stream)
- using (var memoryStream = new MemoryStream())
- {
- await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
- memoryStream.Position = 0;
-
- var savePaths = new List<string>();
- var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
-
- if (response.IsForced)
- {
- saveFileName += ".forced";
- }
-
- saveFileName += "." + response.Format.ToLowerInvariant();
-
- if (saveInMediaFolder)
- {
- savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
- }
-
- savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
-
- await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
- }
+ await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false);
}
catch (RateLimitExceededException)
{
@@ -199,6 +173,47 @@ namespace MediaBrowser.Providers.Subtitles
}
}
+ /// <inheritdoc />
+ public Task UploadSubtitle(Video video, SubtitleResponse response)
+ {
+ var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
+ return TrySaveSubtitle(video, libraryOptions, response);
+ }
+
+ private async Task TrySaveSubtitle(
+ Video video,
+ LibraryOptions libraryOptions,
+ SubtitleResponse response)
+ {
+ var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
+
+ using (var stream = response.Stream)
+ using (var memoryStream = new MemoryStream())
+ {
+ await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ memoryStream.Position = 0;
+
+ var savePaths = new List<string>();
+ var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
+
+ if (response.IsForced)
+ {
+ saveFileName += ".forced";
+ }
+
+ saveFileName += "." + response.Format.ToLowerInvariant();
+
+ if (saveInMediaFolder)
+ {
+ savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
+ }
+
+ savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
+
+ await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
+ }
+ }
+
private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
{
Exception exceptionToThrow = null;
diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs
deleted file mode 100644
index 905cbefd3..000000000
--- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs
+++ /dev/null
@@ -1,229 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.TV
-{
- public class DummySeasonProvider
- {
- private readonly ILogger _logger;
- private readonly ILocalizationManager _localization;
- private readonly ILibraryManager _libraryManager;
- private readonly IFileSystem _fileSystem;
-
- public DummySeasonProvider(
- ILogger logger,
- ILocalizationManager localization,
- ILibraryManager libraryManager,
- IFileSystem fileSystem)
- {
- _logger = logger;
- _localization = localization;
- _libraryManager = libraryManager;
- _fileSystem = fileSystem;
- }
-
- public async Task<bool> Run(Series series, CancellationToken cancellationToken)
- {
- var seasonsRemoved = RemoveObsoleteSeasons(series);
-
- var hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false);
-
- if (hasNewSeasons)
- {
- // var directoryService = new DirectoryService(_fileSystem);
-
- // await series.RefreshMetadata(new MetadataRefreshOptions(directoryService), cancellationToken).ConfigureAwait(false);
-
- // await series.ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(directoryService))
- // .ConfigureAwait(false);
- }
-
- return seasonsRemoved || hasNewSeasons;
- }
-
- private async Task<bool> AddDummySeasonFolders(Series series, CancellationToken cancellationToken)
- {
- var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
- .Cast<Episode>()
- .Where(i => !i.IsInSeasonFolder)
- .ToList();
-
- var hasChanges = false;
-
- List<Season> seasons = null;
-
- // Loop through the unique season numbers
- foreach (var seasonNumber in episodesInSeriesFolder.Select(i => i.ParentIndexNumber ?? -1)
- .Where(i => i >= 0)
- .Distinct()
- .ToList())
- {
- if (seasons == null)
- {
- seasons = series.Children.OfType<Season>().ToList();
- }
-
- var existingSeason = seasons
- .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
-
- if (existingSeason == null)
- {
- await AddSeason(series, seasonNumber, false, cancellationToken).ConfigureAwait(false);
- hasChanges = true;
- seasons = null;
- }
- else if (existingSeason.IsVirtualItem)
- {
- existingSeason.IsVirtualItem = false;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- seasons = null;
- }
- }
-
- // Unknown season - create a dummy season to put these under
- if (episodesInSeriesFolder.Any(i => !i.ParentIndexNumber.HasValue))
- {
- if (seasons == null)
- {
- seasons = series.Children.OfType<Season>().ToList();
- }
-
- var existingSeason = seasons
- .FirstOrDefault(i => !i.IndexNumber.HasValue);
-
- if (existingSeason == null)
- {
- await AddSeason(series, null, false, cancellationToken).ConfigureAwait(false);
-
- hasChanges = true;
- seasons = null;
- }
- else if (existingSeason.IsVirtualItem)
- {
- existingSeason.IsVirtualItem = false;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- seasons = null;
- }
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Adds the season.
- /// </summary>
- public async Task<Season> AddSeason(
- Series series,
- int? seasonNumber,
- bool isVirtualItem,
- CancellationToken cancellationToken)
- {
- string seasonName;
- if (seasonNumber == null)
- {
- seasonName = _localization.GetLocalizedString("NameSeasonUnknown");
- }
- else if (seasonNumber == 0)
- {
- seasonName = _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName;
- }
- else
- {
- seasonName = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("NameSeasonNumber"),
- seasonNumber.Value);
- }
-
- _logger.LogInformation("Creating Season {0} entry for {1}", seasonName, series.Name);
-
- var season = new Season
- {
- Name = seasonName,
- IndexNumber = seasonNumber,
- Id = _libraryManager.GetNewItemId(
- series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
- typeof(Season)),
- IsVirtualItem = isVirtualItem,
- SeriesId = series.Id,
- SeriesName = series.Name
- };
-
- season.SetParent(series);
-
- series.AddChild(season, cancellationToken);
-
- await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
-
- return season;
- }
-
- private bool RemoveObsoleteSeasons(Series series)
- {
- var existingSeasons = series.Children.OfType<Season>().ToList();
-
- var physicalSeasons = existingSeasons
- .Where(i => i.LocationType != LocationType.Virtual)
- .ToList();
-
- var virtualSeasons = existingSeasons
- .Where(i => i.LocationType == LocationType.Virtual)
- .ToList();
-
- var seasonsToRemove = virtualSeasons
- .Where(i =>
- {
- if (i.IndexNumber.HasValue)
- {
- var seasonNumber = i.IndexNumber.Value;
-
- // If there's a physical season with the same number, delete it
- if (physicalSeasons.Any(p => p.IndexNumber.HasValue && (p.IndexNumber.Value == seasonNumber)))
- {
- return true;
- }
- }
-
- // If there are no episodes with this season number, delete it
- if (!i.GetEpisodes().Any())
- {
- return true;
- }
-
- return false;
- })
- .ToList();
-
- var hasChanges = false;
-
- foreach (var seasonToRemove in seasonsToRemove)
- {
- _logger.LogInformation("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber);
-
- _libraryManager.DeleteItem(
- seasonToRemove,
- new DeleteOptions
- {
- DeleteFileLocation = true
- },
- false);
-
- hasChanges = true;
- }
-
- return hasChanges;
- }
- }
-}
diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
deleted file mode 100644
index c833b1227..000000000
--- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
+++ /dev/null
@@ -1,404 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Providers.Plugins.TheTvdb;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.TV
-{
- public class MissingEpisodeProvider
- {
- private const double UnairedEpisodeThresholdDays = 2;
-
- private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
- private readonly IFileSystem _fileSystem;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public MissingEpisodeProvider(
- ILogger logger,
- IServerConfigurationManager config,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- IFileSystem fileSystem,
- TvdbClientManager tvdbClientManager)
- {
- _logger = logger;
- _config = config;
- _libraryManager = libraryManager;
- _localization = localization;
- _fileSystem = fileSystem;
- _tvdbClientManager = tvdbClientManager;
- }
-
- public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken)
- {
- var tvdbIdString = series.GetProviderId(MetadataProvider.Tvdb);
- if (string.IsNullOrEmpty(tvdbIdString))
- {
- return false;
- }
-
- var episodes = await _tvdbClientManager.GetAllEpisodesAsync(
- int.Parse(tvdbIdString, CultureInfo.InvariantCulture),
- series.GetPreferredMetadataLanguage(),
- cancellationToken).ConfigureAwait(false);
-
- var episodeLookup = episodes
- .Select(i =>
- {
- if (!DateTime.TryParse(i.FirstAired, out var firstAired))
- {
- firstAired = default;
- }
-
- var seasonNumber = i.AiredSeason.GetValueOrDefault(-1);
- var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1);
- return (seasonNumber, episodeNumber, firstAired);
- })
- .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1)
- .OrderBy(i => i.seasonNumber)
- .ThenBy(i => i.episodeNumber)
- .ToList();
-
- var allRecursiveChildren = series.GetRecursiveChildren();
-
- var hasBadData = HasInvalidContent(allRecursiveChildren);
-
- // Be conservative here to avoid creating missing episodes for ones they already have
- var addMissingEpisodes = !hasBadData && _libraryManager.GetLibraryOptions(series).ImportMissingEpisodes;
-
- var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(allRecursiveChildren, episodeLookup);
-
- if (anySeasonsRemoved)
- {
- // refresh this
- allRecursiveChildren = series.GetRecursiveChildren();
- }
-
- var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(allRecursiveChildren, episodeLookup, addMissingEpisodes);
-
- if (anyEpisodesRemoved)
- {
- // refresh this
- allRecursiveChildren = series.GetRecursiveChildren();
- }
-
- var hasNewEpisodes = false;
-
- if (addNewItems && series.IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(series), TvdbSeriesProvider.Current.Name))
- {
- hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, episodeLookup, cancellationToken)
- .ConfigureAwait(false);
- }
-
- if (hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved)
- {
- return true;
- }
-
- return false;
- }
-
- /// <summary>
- /// Returns true if a series has any seasons or episodes without season or episode numbers
- /// If this data is missing no virtual items will be added in order to prevent possible duplicates.
- /// </summary>
- private bool HasInvalidContent(IList<BaseItem> allItems)
- {
- return allItems.OfType<Season>().Any(i => !i.IndexNumber.HasValue) ||
- allItems.OfType<Episode>().Any(i =>
- {
- if (!i.ParentIndexNumber.HasValue)
- {
- return true;
- }
-
- // You could have episodes under season 0 with no number
- return false;
- });
- }
-
- private async Task<bool> AddMissingEpisodes(
- Series series,
- IEnumerable<BaseItem> allItems,
- bool addMissingEpisodes,
- IReadOnlyCollection<(int seasonNumber, int episodenumber, DateTime firstAired)> episodeLookup,
- CancellationToken cancellationToken)
- {
- var existingEpisodes = allItems.OfType<Episode>().ToList();
-
- var seasonCounts = episodeLookup.GroupBy(e => e.seasonNumber).ToDictionary(g => g.Key, g => g.Count());
-
- var hasChanges = false;
-
- foreach (var tuple in episodeLookup)
- {
- if (tuple.seasonNumber <= 0 || tuple.episodenumber <= 0)
- {
- // Ignore episode/season zeros
- continue;
- }
-
- var existingEpisode = GetExistingEpisode(existingEpisodes, seasonCounts, tuple);
-
- if (existingEpisode != null)
- {
- continue;
- }
-
- var airDate = tuple.firstAired;
-
- var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays);
-
- if ((airDate < now && addMissingEpisodes) || airDate > now)
- {
- // tvdb has a lot of nearly blank episodes
- _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber);
- await AddEpisode(series, tuple.seasonNumber, tuple.episodenumber, cancellationToken).ConfigureAwait(false);
-
- hasChanges = true;
- }
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Removes the virtual entry after a corresponding physical version has been added.
- /// </summary>
- private bool RemoveObsoleteOrMissingEpisodes(
- IEnumerable<BaseItem> allRecursiveChildren,
- IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup,
- bool allowMissingEpisodes)
- {
- var existingEpisodes = allRecursiveChildren.OfType<Episode>();
-
- var physicalEpisodes = new List<Episode>();
- var virtualEpisodes = new List<Episode>();
- foreach (var episode in existingEpisodes)
- {
- if (episode.LocationType == LocationType.Virtual)
- {
- virtualEpisodes.Add(episode);
- }
- else
- {
- physicalEpisodes.Add(episode);
- }
- }
-
- var episodesToRemove = virtualEpisodes
- .Where(i =>
- {
- if (!i.IndexNumber.HasValue || !i.ParentIndexNumber.HasValue)
- {
- return true;
- }
-
- var seasonNumber = i.ParentIndexNumber.Value;
- var episodeNumber = i.IndexNumber.Value;
-
- // If there's a physical episode with the same season and episode number, delete it
- if (physicalEpisodes.Any(p =>
- p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber &&
- p.ContainsEpisodeNumber(episodeNumber)))
- {
- return true;
- }
-
- // If the episode no longer exists in the remote lookup, delete it
- if (!episodeLookup.Any(e => e.seasonNumber == seasonNumber && e.episodeNumber == episodeNumber))
- {
- return true;
- }
-
- // If it's missing, but not unaired, remove it
- return !allowMissingEpisodes && i.IsMissingEpisode &&
- (!i.PremiereDate.HasValue ||
- i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) <
- DateTime.Now.Date);
- });
-
- var hasChanges = false;
-
- foreach (var episodeToRemove in episodesToRemove)
- {
- _libraryManager.DeleteItem(
- episodeToRemove,
- new DeleteOptions
- {
- DeleteFileLocation = true
- },
- false);
-
- hasChanges = true;
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Removes the obsolete or missing seasons.
- /// </summary>
- /// <param name="allRecursiveChildren">All recursive children.</param>
- /// <param name="episodeLookup">The episode lookup.</param>
- /// <returns><see cref="bool" />.</returns>
- private bool RemoveObsoleteOrMissingSeasons(
- IList<BaseItem> allRecursiveChildren,
- IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup)
- {
- var existingSeasons = allRecursiveChildren.OfType<Season>().ToList();
-
- var physicalSeasons = new List<Season>();
- var virtualSeasons = new List<Season>();
- foreach (var season in existingSeasons)
- {
- if (season.LocationType == LocationType.Virtual)
- {
- virtualSeasons.Add(season);
- }
- else
- {
- physicalSeasons.Add(season);
- }
- }
-
- var allEpisodes = allRecursiveChildren.OfType<Episode>().ToList();
-
- var seasonsToRemove = virtualSeasons
- .Where(i =>
- {
- if (i.IndexNumber.HasValue)
- {
- var seasonNumber = i.IndexNumber.Value;
-
- // If there's a physical season with the same number, delete it
- if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal)))
- {
- return true;
- }
-
- // If the season no longer exists in the remote lookup, delete it, but only if an existing episode doesn't require it
- return episodeLookup.All(e => e.seasonNumber != seasonNumber) && allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder);
- }
-
- // Season does not have a number
- // Remove if there are no episodes directly in series without a season number
- return allEpisodes.All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder);
- });
-
- var hasChanges = false;
-
- foreach (var seasonToRemove in seasonsToRemove)
- {
- _libraryManager.DeleteItem(
- seasonToRemove,
- new DeleteOptions
- {
- DeleteFileLocation = true
- },
- false);
-
- hasChanges = true;
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Adds the episode.
- /// </summary>
- /// <param name="series">The series.</param>
- /// <param name="seasonNumber">The season number.</param>
- /// <param name="episodeNumber">The episode number.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
- {
- var season = series.Children.OfType<Season>()
- .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
-
- if (season == null)
- {
- var provider = new DummySeasonProvider(_logger, _localization, _libraryManager, _fileSystem);
- season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false);
- }
-
- var name = "Episode " + episodeNumber.ToString(CultureInfo.InvariantCulture);
-
- var episode = new Episode
- {
- Name = name,
- IndexNumber = episodeNumber,
- ParentIndexNumber = seasonNumber,
- Id = _libraryManager.GetNewItemId(
- series.Id + seasonNumber.ToString(CultureInfo.InvariantCulture) + name,
- typeof(Episode)),
- IsVirtualItem = true,
- SeasonId = season?.Id ?? Guid.Empty,
- SeriesId = series.Id
- };
-
- season.AddChild(episode, cancellationToken);
-
- await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Gets the existing episode.
- /// </summary>
- /// <param name="existingEpisodes">The existing episodes.</param>
- /// <param name="seasonCounts"></param>
- /// <param name="episodeTuple"></param>
- /// <returns>Episode.</returns>
- private Episode GetExistingEpisode(
- IEnumerable<Episode> existingEpisodes,
- IReadOnlyDictionary<int, int> seasonCounts,
- (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple)
- {
- var seasonNumber = episodeTuple.seasonNumber;
- var episodeNumber = episodeTuple.episodeNumber;
-
- while (true)
- {
- var episode = GetExistingEpisode(existingEpisodes, seasonNumber, episodeNumber);
- if (episode != null)
- {
- return episode;
- }
-
- seasonNumber--;
-
- if (seasonCounts.ContainsKey(seasonNumber))
- {
- episodeNumber += seasonCounts[seasonNumber];
- }
- else
- {
- break;
- }
- }
-
- return null;
- }
-
- private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, int season, int episode)
- => existingEpisodes.FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode));
- }
-}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index a2c0e62c1..c8fc568a2 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,65 +1,26 @@
#pragma warning disable CS1591
-using System;
-using System.Threading;
-using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
-using MediaBrowser.Providers.Plugins.TheTvdb;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.TV
{
public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{
- private readonly ILocalizationManager _localization;
- private readonly TvdbClientManager _tvdbClientManager;
-
public SeriesMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger<SeriesMetadataService> logger,
IProviderManager providerManager,
IFileSystem fileSystem,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- TvdbClientManager tvdbClientManager)
+ ILibraryManager libraryManager)
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
- _localization = localization;
- _tvdbClientManager = tvdbClientManager;
- }
-
- /// <inheritdoc />
- protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
- {
- await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
-
- var seasonProvider = new DummySeasonProvider(Logger, _localization, LibraryManager, FileSystem);
- await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false);
-
- // TODO why does it not register this itself omg
- var provider = new MissingEpisodeProvider(
- Logger,
- ServerConfigurationManager,
- LibraryManager,
- _localization,
- FileSystem,
- _tvdbClientManager);
-
- try
- {
- await provider.Run(item, true, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path);
- }
}
/// <inheritdoc />
diff --git a/apiclient/.openapi-generator-ignore b/apiclient/.openapi-generator-ignore
new file mode 100644
index 000000000..f3802cf54
--- /dev/null
+++ b/apiclient/.openapi-generator-ignore
@@ -0,0 +1,2 @@
+# Prevent generator from creating these files:
+git_push.sh
diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh
new file mode 100644
index 000000000..8c4d74282
--- /dev/null
+++ b/apiclient/templates/typescript/axios/generate.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+artifactsDirectory="${1}"
+buildNumber="${2}"
+if [[ -n ${buildNumber} ]]; then
+ # Unstable build
+ additionalProperties=",snapshotVersion=-SNAPSHOT.${buildNumber},npmRepository=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/"
+else
+ # Stable build
+ additionalProperties=""
+fi
+
+java -jar openapi-generator-cli.jar generate \
+ --input-spec ${artifactsDirectory}/openapispec/openapi.json \
+ --generator-name typescript-axios \
+ --output ./apiclient/generated/typescript/axios \
+ --template-dir ./apiclient/templates/typescript/axios \
+ --ignore-file-override ./apiclient/.openapi-generator-ignore \
+ --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios"${additionalProperties}
diff --git a/apiclient/templates/typescript/axios/package.mustache b/apiclient/templates/typescript/axios/package.mustache
new file mode 100644
index 000000000..7bfab08cb
--- /dev/null
+++ b/apiclient/templates/typescript/axios/package.mustache
@@ -0,0 +1,30 @@
+{
+ "name": "@jellyfin/client-axios",
+ "version": "10.7.0{{snapshotVersion}}",
+ "description": "Jellyfin api client using axios",
+ "author": "Jellyfin Contributors",
+ "keywords": [
+ "axios",
+ "typescript",
+ "jellyfin"
+ ],
+ "license": "GPL-3.0-only",
+ "main": "./dist/index.js",
+ "typings": "./dist/index.d.ts",
+ "scripts": {
+ "build": "tsc --outDir dist/",
+ "prepublishOnly": "npm run build"
+ },
+ "dependencies": {
+ "axios": "^0.19.2"
+ },
+ "devDependencies": {
+ "@types/node": "^12.11.5",
+ "typescript": "^3.6.4"
+ }{{#npmRepository}},{{/npmRepository}}
+{{#npmRepository}}
+ "publishConfig": {
+ "registry": "{{npmRepository}}"
+ }
+{{/npmRepository}}
+}
diff --git a/debian/control b/debian/control
index 39c2aa055..9216d24fe 100644
--- a/debian/control
+++ b/debian/control
@@ -20,7 +20,6 @@ Breaks: jellyfin (<<10.6.0)
Architecture: any
Depends: at,
libsqlite3-0,
- jellyfin-ffmpeg (>= 4.2.1-2),
libfontconfig1,
libfreetype6,
libssl1.1
diff --git a/debian/postrm b/debian/postrm
index 1d00a984e..3d56a5f1e 100644
--- a/debian/postrm
+++ b/debian/postrm
@@ -25,7 +25,7 @@ case "$1" in
purge)
echo PURGE | debconf-communicate $NAME > /dev/null 2>&1 || true
- if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.connf" ]]; then
+ if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
update-rc.d jellyfin remove >/dev/null 2>&1 || true
fi
@@ -54,7 +54,7 @@ case "$1" in
rm -rf $PROGRAMDATA
fi
# Remove binary symlink
- [[ -f /usr/bin/jellyfin ]] && rm /usr/bin/jellyfin
+ rm -f /usr/bin/jellyfin
# Remove sudoers config
[[ -f /etc/sudoers.d/jellyfin-sudoers ]] && rm /etc/sudoers.d/jellyfin-sudoers
# Remove anything at the default locations; catches situations where the user moved the defaults
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index 7202c5883..aaca8fe01 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index e9f30213f..594da04ce 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index 91a8a6e7a..3e6e2d0d7 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 828d5c2cf..f98881ebf 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index 0b2a0fe5f..ec9d2d8c7 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 7d5de230f..3523f8ace 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 9c63f43df..0a365e1ae 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 51612dd44..ab3ec9b9f 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 4ed7f8687..fa41bdf48 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index 5671cc598..7216b2363 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64
index afe490576..a9daa6a23 100755
--- a/deployment/build.windows.amd64
+++ b/deployment/build.windows.amd64
@@ -35,10 +35,6 @@ unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmp
cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir}
rm -rf ${addin_build_dir}
-# Prepare scripts
-cp ${SOURCE_DIR}/windows/legacy/install-jellyfin.ps1 ${output_dir}/install-jellyfin.ps1
-cp ${SOURCE_DIR}/windows/legacy/install.bat ${output_dir}/install.bat
-
# Create zip package
pushd dist
zip -qr jellyfin-server_${version}.portable.zip jellyfin-server_${version}
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index bfb2b3be2..93fb9fb41 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -79,15 +79,7 @@ EOF
%files server
%attr(755,root,root) %{_bindir}/jellyfin
-%{_libdir}/jellyfin/*.json
-%{_libdir}/jellyfin/*.dll
-%{_libdir}/jellyfin/*.so
-%{_libdir}/jellyfin/*.a
-%{_libdir}/jellyfin/createdump
-%{_libdir}/jellyfin/*.xml
-%{_libdir}/jellyfin/wwwroot/api-docs/*
-%{_libdir}/jellyfin/wwwroot/api-docs/redoc/*
-%{_libdir}/jellyfin/wwwroot/api-docs/swagger/*
+%{_libdir}/jellyfin/*
# Needs 755 else only root can run it since binary build by dotnet is 722
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
%{_libdir}/jellyfin/SOS_README.md
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index 4ea5094b6..a46d94457 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Auth;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
@@ -68,14 +69,14 @@ namespace Jellyfin.Api.Tests.Auth
}
[Fact]
- public async Task HandleAuthenticateAsyncShouldFailOnSecurityException()
+ public async Task HandleAuthenticateAsyncShouldFailOnAuthenticationException()
{
var errorMessage = _fixture.Create<string>();
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>()))
- .Throws(new SecurityException(errorMessage));
+ .Throws(new AuthenticationException(errorMessage));
var authenticateResult = await _sut.AuthenticateAsync();
@@ -128,6 +129,7 @@ namespace Jellyfin.Api.Tests.Auth
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
authorizationInfo.User = _fixture.Create<User>();
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
+ authorizationInfo.IsApiKey = false;
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index e3a7a5428..0236f2ac1 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -13,16 +13,16 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.13.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.13.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
+ <PackageReference Include="AutoFixture" Version="4.14.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
- <PackageReference Include="Moq" Version="4.14.5" />
+ <PackageReference Include="Moq" Version="4.14.7" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
new file mode 100644
index 000000000..89c7d62f7
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Api.ModelBinders;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Primitives;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.ModelBinders
+{
+ public sealed class CommaDelimitedArrayModelBinderTests
+ {
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new[] { "lol", "xd" };
+ var queryParamString = "lol,xd";
+ var queryParamType = typeof(string[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((string[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new[] { 42, 0 };
+ var queryParamString = "42,0";
+ var queryParamType = typeof(int[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((int[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new[] { TestType.How, TestType.Much };
+ var queryParamString = "How,Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new[] { TestType.How, TestType.Much };
+ var queryParamString = "How,,Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new[] { TestType.How, TestType.Much };
+ var queryParamString1 = "How";
+ var queryParamString2 = "Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>
+ {
+ { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = Array.Empty<TestType>();
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>
+ {
+ { queryParamName, new StringValues(value: null) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid()
+ {
+ var queryParamName = "test";
+ var queryParamString = "🔥,😢";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ await Assert.ThrowsAsync<FormatException>(act);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2()
+ {
+ var queryParamName = "test";
+ var queryParamString1 = "How";
+ var queryParamString2 = "😱";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>
+ {
+ { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ await Assert.ThrowsAsync<FormatException>(act);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs
new file mode 100644
index 000000000..544a74637
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Jellyfin.Api.Tests.ModelBinders
+{
+ public enum TestType
+ {
+#pragma warning disable SA1602 // Enumeration items should be documented
+ How,
+ Much,
+ Is,
+ The,
+ Fish
+#pragma warning restore SA1602 // Enumeration items should be documented
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
index a4dd4e409..c4ce39885 100644
--- a/tests/Jellyfin.Api.Tests/TestHelpers.cs
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -45,7 +45,7 @@ namespace Jellyfin.Api.Tests
{
new Claim(ClaimTypes.Role, role),
new Claim(ClaimTypes.Name, "jellyfin"),
- new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.UserId, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.Device, "test"),
new Claim(InternalClaimTypes.Client, "test"),
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
new file mode 100644
index 000000000..0d2bdd1af
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
@@ -0,0 +1,92 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Jellyfin.Common.Tests.Models;
+using MediaBrowser.Model.Session;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public static class JsonCommaDelimitedArrayTests
+ {
+ [Fact]
+ public static void Deserialize_String_Valid_Success()
+ {
+ var desiredValue = new GenericBodyArrayModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_String_Space_Valid_Success()
+ {
+ var desiredValue = new GenericBodyArrayModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Valid_Success()
+ {
+ var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Space_Valid_Success()
+ {
+ var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_String_Array_Valid_Success()
+ {
+ var desiredValue = new GenericBodyArrayModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Array_Valid_Success()
+ {
+ var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs
new file mode 100644
index 000000000..34ad9bac7
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs
@@ -0,0 +1,92 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Jellyfin.Common.Tests.Models;
+using MediaBrowser.Model.Session;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public static class JsonCommaDelimitedIReadOnlyListTests
+ {
+ [Fact]
+ public static void Deserialize_String_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_String_Space_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Space_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_String_Array_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<string>
+ {
+ Value = new[] { "a", "b", "c" }
+ };
+
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static void Deserialize_GenericCommandType_Array_Valid_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonStringEnumConverter());
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
new file mode 100644
index 000000000..d9e66d677
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using MediaBrowser.Common.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Extensions
+{
+ public static class JsonGuidConverterTests
+ {
+ [Fact]
+ public static void Deserialize_Valid_Success()
+ {
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonGuidConverter());
+ Guid value = JsonSerializer.Deserialize<Guid>(@"""a852a27afe324084ae66db579ee3ee18""", options);
+ Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value);
+
+ value = JsonSerializer.Deserialize<Guid>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", options);
+ Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
+ }
+
+ [Fact]
+ public static void Roundtrip_Valid_Success()
+ {
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonGuidConverter());
+ Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18");
+ string value = JsonSerializer.Serialize(guid, options);
+ Assert.Equal(guid, JsonSerializer.Deserialize<Guid>(value, options));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs
new file mode 100644
index 000000000..276e1bfbe
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs
@@ -0,0 +1,20 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+using MediaBrowser.Common.Json.Converters;
+
+namespace Jellyfin.Common.Tests.Models
+{
+ /// <summary>
+ /// The generic body model.
+ /// </summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ public class GenericBodyArrayModel<T>
+ {
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")]
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public T[] Value { get; set; } = default!;
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs
new file mode 100644
index 000000000..627454b25
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using MediaBrowser.Common.Json.Converters;
+
+namespace Jellyfin.Common.Tests.Models
+{
+ /// <summary>
+ /// The generic body <c>IReadOnlyList</c> model.
+ /// </summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ public class GenericBodyIReadOnlyListModel<T>
+ {
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<T> Value { get; set; } = default!;
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index 83d44721c..673289436 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Xunit;
@@ -42,16 +43,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
[Theory]
[MemberData(nameof(GetResolveFileTestData))]
- public void ResolveFile_ValidFileName_Success(AudioBookFileInfo expectedResult)
+ public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult)
{
var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path);
Assert.NotNull(result);
- Assert.Equal(result.Path, expectedResult.Path);
- Assert.Equal(result.Container, expectedResult.Container);
- Assert.Equal(result.ChapterNumber, expectedResult.ChapterNumber);
- Assert.Equal(result.PartNumber, expectedResult.PartNumber);
- Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+ Assert.Equal(result!.Path, expectedResult.Path);
+ Assert.Equal(result!.Container, expectedResult.Container);
+ Assert.Equal(result!.ChapterNumber, expectedResult.ChapterNumber);
+ Assert.Equal(result!.PartNumber, expectedResult.PartNumber);
+ Assert.Equal(result!.IsDirectory, expectedResult.IsDirectory);
+ }
+
+ [Fact]
+ public void Resolve_EmptyFileName_ArgumentException()
+ {
+ Assert.Throws<ArgumentException>(() => new AudioBookResolver(_namingOptions).Resolve(string.Empty));
}
}
}
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 d1679c279..05323490e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -14,10 +14,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.13.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
+ <PackageReference Include="AutoFixture" Version="4.14.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
- <PackageReference Include="Moq" Version="4.14.5" />
+ <PackageReference Include="Moq" Version="4.14.7" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1
deleted file mode 100644
index b65e619ee..000000000
--- a/windows/build-jellyfin.ps1
+++ /dev/null
@@ -1,190 +0,0 @@
-[CmdletBinding()]
-param(
- [switch]$MakeNSIS,
- [switch]$InstallNSIS,
- [switch]$InstallFFMPEG,
- [switch]$InstallNSSM,
- [switch]$SkipJellyfinBuild,
- [switch]$GenerateZip,
- [string]$InstallLocation = "./dist/jellyfin-win-nsis",
- [string]$UXLocation = "../jellyfin-ux",
- [switch]$InstallTrayApp,
- [ValidateSet('Debug','Release')][string]$BuildType = 'Release',
- [ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal',
- [ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win',
- [ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64'
-)
-
-$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars.
-
-#PowershellCore and *nix check to make determine which temp dir to use.
-if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){
- $TempDir = mktemp -d
-}else{
- $TempDir = $env:Temp
-}
-#Create staging dir
-New-Item -ItemType Directory -Force -Path $InstallLocation
-$ResolvedInstallLocation = Resolve-Path $InstallLocation
-$ResolvedUXLocation = Resolve-Path $UXLocation
-
-function Build-JellyFin {
- if(($Architecture -eq 'arm64') -and ($WindowsVersion -ne 'win10')){
- Write-Error "arm64 only supported with Windows10 Version"
- exit
- }
- if(($Architecture -eq 'arm') -and ($WindowsVersion -notin @('win10','win81','win8'))){
- Write-Error "arm only supported with Windows 8 or higher"
- exit
- }
- Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture"
- Write-Verbose "InstallLocation: $ResolvedInstallLocation"
- Write-Verbose "DotNetVerbosity: $DotNetVerbosity"
- dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=true -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server
-}
-
-function Install-FFMPEG {
- param(
- [string]$ResolvedInstallLocation,
- [string]$Architecture,
- [string]$FFMPEGVersionX86 = "ffmpeg-4.3-win32-shared"
- )
- Write-Verbose "Checking Architecture"
- if($Architecture -notin @('x86','x64')){
- Write-Warning "No builds available for your selected architecture of $Architecture"
- Write-Warning "FFMPEG will not be installed"
- }elseif($Architecture -eq 'x64'){
- Write-Verbose "Downloading 64 bit FFMPEG"
- Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
- }else{
- Write-Verbose "Downloading 32 bit FFMPEG"
- Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
- }
-
- Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose
- if($Architecture -eq 'x64'){
- Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/ffmpeg" | ForEach-Object {
- Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
- }
- }else{
- Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object {
- Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
- }
- }
- Remove-Item "$tempdir/ffmpeg/" -Recurse -Force -ErrorAction Continue | Write-Verbose
- Remove-Item "$tempdir/ffmpeg.zip" -Force -ErrorAction Continue | Write-Verbose
-}
-
-function Install-NSSM {
- param(
- [string]$ResolvedInstallLocation,
- [string]$Architecture
- )
- Write-Verbose "Checking Architecture"
- if($Architecture -notin @('x86','x64')){
- Write-Warning "No builds available for your selected architecture of $Architecture"
- Write-Warning "NSSM will not be installed"
- }else{
- Write-Verbose "Downloading NSSM"
- # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
- # Temporary workaround, file is hosted in an azure blob with a custom domain in front for brevity
- Invoke-WebRequest -Uri http://files.evilt.win/nssm/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose
- }
-
- Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" -Force | Write-Verbose
- if($Architecture -eq 'x64'){
- Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win64" | ForEach-Object {
- Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
- }
- }else{
- Write-Verbose "Copying Binaries to Jellyfin location"
- Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win32" | ForEach-Object {
- Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
- }
- }
- Remove-Item "$tempdir/nssm/" -Recurse -Force -ErrorAction Continue | Write-Verbose
- Remove-Item "$tempdir/nssm.zip" -Force -ErrorAction Continue | Write-Verbose
-}
-
-function Make-NSIS {
- param(
- [string]$ResolvedInstallLocation
- )
-
- $env:InstallLocation = $ResolvedInstallLocation
- if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
- & "$tempdir/nsis/nsis-3.04/makensis.exe" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi"
- } else {
- & "makensis" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi"
- }
- Copy-Item .\deployment\windows\jellyfin_*.exe $ResolvedInstallLocation\..\
-}
-
-
-function Install-NSIS {
- Write-Verbose "Downloading NSIS"
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
- Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose
-
- Expand-Archive "$tempdir/nsis.zip" -DestinationPath "$tempdir/nsis/" -Force | Write-Verbose
-}
-
-function Cleanup-NSIS {
- Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose
- Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose
-}
-
-function Install-TrayApp {
- param(
- [string]$ResolvedInstallLocation,
- [string]$Architecture
- )
- Write-Verbose "Checking Architecture"
- if($Architecture -ne 'x64'){
- Write-Warning "No builds available for your selected architecture of $Architecture"
- Write-Warning "The tray app will not be available."
- }else{
- Write-Verbose "Downloading Tray App and copying to Jellyfin location"
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
- Invoke-WebRequest -Uri https://github.com/jellyfin/jellyfin-windows-tray/releases/latest/download/JellyfinTray.exe -UseBasicParsing -OutFile "$installLocation/JellyfinTray.exe" | Write-Verbose
- }
-}
-
-if(-not $SkipJellyfinBuild.IsPresent -and -not ($InstallNSIS -eq $true)){
- Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture"
- Build-JellyFin
-}
-if($InstallFFMPEG.IsPresent -or ($InstallFFMPEG -eq $true)){
- Write-Verbose "Starting FFMPEG Install"
- Install-FFMPEG $ResolvedInstallLocation $Architecture
-}
-if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){
- Write-Verbose "Starting NSSM Install"
- Install-NSSM $ResolvedInstallLocation $Architecture
-}
-if($InstallTrayApp.IsPresent -or ($InstallTrayApp -eq $true)){
- Write-Verbose "Downloading Windows Tray App"
- Install-TrayApp $ResolvedInstallLocation $Architecture
-}
-#Copy-Item .\deployment\windows\install-jellyfin.ps1 $ResolvedInstallLocation\install-jellyfin.ps1
-#Copy-Item .\deployment\windows\install.bat $ResolvedInstallLocation\install.bat
-Copy-Item .\LICENSE $ResolvedInstallLocation\LICENSE
-if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
- Write-Verbose "Installing NSIS"
- Install-NSIS
-}
-if($MakeNSIS.IsPresent -or ($MakeNSIS -eq $true)){
- Write-Verbose "Starting NSIS Package creation"
- Make-NSIS $ResolvedInstallLocation
-}
-if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
- Write-Verbose "Cleanup NSIS"
- Cleanup-NSIS
-}
-if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){
- Compress-Archive -Path $ResolvedInstallLocation -DestinationPath "$ResolvedInstallLocation/jellyfin.zip" -Force
-}
-Write-Verbose "Finished"
diff --git a/windows/dependencies.txt b/windows/dependencies.txt
deleted file mode 100644
index 16f77cce7..000000000
--- a/windows/dependencies.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-dotnet
-nsis
diff --git a/windows/dialogs/confirmation.nsddef b/windows/dialogs/confirmation.nsddef
deleted file mode 100644
index 969ebacd6..000000000
--- a/windows/dialogs/confirmation.nsddef
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-This file was created by NSISDialogDesigner 1.4.4.0
-http://coolsoft.altervista.org/nsisdialogdesigner
-Do not edit manually!
--->
-<Dialog Name="confirmation" Title="Confirmation Page" Subtitle="Please confirm your choices for Jellyfin Server installation" GenerateShowFunction="False">
- <HeaderCustomScript>!include "helpers\StrSlash.nsh"</HeaderCustomScript>
- <CreateFunctionCustomScript>${StrSlash} '$0' $INSTDIR
-
- ${StrSlash} '$1' $_JELLYFINDATADIR_
-
- ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
- \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
- Installation Folder:\b0 $0\line\b \
- Service install:\b0 $_INSTALLSERVICE_\line\b \
- Service start:\b0 $_SERVICESTART_\line\b \
- Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \
- Jellyfin Data Folder:\b0 $1\par \
-\
- \pard\sa200\sl276\slmult1\f1\lang1043\par \
- }"</CreateFunctionCustomScript>
- <RichText Name="ConfirmRichText" Location="12, 12" Size="426, 204" TabIndex="0" ExStyle="WS_EX_STATICEDGE" />
-</Dialog>
diff --git a/windows/dialogs/confirmation.nsdinc b/windows/dialogs/confirmation.nsdinc
deleted file mode 100644
index f00e9b43a..000000000
--- a/windows/dialogs/confirmation.nsdinc
+++ /dev/null
@@ -1,61 +0,0 @@
-; =========================================================
-; This file was generated by NSISDialogDesigner 1.4.4.0
-; http://coolsoft.altervista.org/nsisdialogdesigner
-;
-; Do not edit it manually, use NSISDialogDesigner instead!
-; Modified by EraYaN (2019-09-01)
-; =========================================================
-
-; handle variables
-Var hCtl_confirmation
-Var hCtl_confirmation_ConfirmRichText
-
-; HeaderCustomScript
-!include "helpers\StrSlash.nsh"
-
-
-
-; dialog create function
-Function fnc_confirmation_Create
-
- ; === confirmation (type: Dialog) ===
- nsDialogs::Create 1018
- Pop $hCtl_confirmation
- ${If} $hCtl_confirmation == error
- Abort
- ${EndIf}
- !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation"
-
- ; === ConfirmRichText (type: RichText) ===
- nsDialogs::CreateControl /NOUNLOAD "RichEdit20A" ${ES_READONLY}|${WS_VISIBLE}|${WS_CHILD}|${WS_TABSTOP}|${WS_VSCROLL}|${ES_MULTILINE}|${ES_WANTRETURN} ${WS_EX_STATICEDGE} 8u 7u 280u 126u ""
- Pop $hCtl_confirmation_ConfirmRichText
- ${NSD_AddExStyle} $hCtl_confirmation_ConfirmRichText ${WS_EX_STATICEDGE}
-
- ; CreateFunctionCustomScript
- ${StrSlash} '$0' $INSTDIR
-
- ${StrSlash} '$1' $_JELLYFINDATADIR_
-
- ${If} $_INSTALLSERVICE_ == "Yes"
- ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
- \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
- Installation Folder:\b0 $0\line\b \
- Service install:\b0 $_INSTALLSERVICE_\line\b \
- Service start:\b0 $_SERVICESTART_\line\b \
- Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \
- Jellyfin Data Folder:\b0 $1\par \
- \
- \pard\sa200\sl276\slmult1\f1\lang1043\par \
- }"
- ${Else}
- ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
- \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
- Installation Folder:\b0 $0\line\b \
- Service install:\b0 $_INSTALLSERVICE_\line\b \
- Jellyfin Data Folder:\b0 $1\par \
- \
- \pard\sa200\sl276\slmult1\f1\lang1043\par \
- }"
- ${EndIf}
-
-FunctionEnd
diff --git a/windows/dialogs/service-config.nsddef b/windows/dialogs/service-config.nsddef
deleted file mode 100644
index 3509ada24..000000000
--- a/windows/dialogs/service-config.nsddef
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-This file was created by NSISDialogDesigner 1.4.4.0
-http://coolsoft.altervista.org/nsisdialogdesigner
-Do not edit manually!
--->
-<Dialog Name="service_config" Title="CoOnfigure the service" Subtitle="This controls what type of access the server gets to this system." GenerateShowFunction="False">
- <CheckBox Name="StartServiceAfterInstall" Location="12, 192" Size="426, 24" Text="Start Service after Install" Checked="True" TabIndex="0" />
- <Label Name="LocalSystemAccountLabel" Location="12, 115" Size="426, 46" Text="The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." TabIndex="1" />
- <Label Name="NetworkServiceAccountLabel" Location="12, 39" Size="426, 46" Text="The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." TabIndex="2" />
- <RadioButton Name="UseLocalSystemAccount" Location="12, 88" Size="426, 24" Text="Use Local System account" TabIndex="3" />
- <RadioButton Name="UseNetworkServiceAccount" Location="12, 12" Size="426, 24" Text="Use Network Service account (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="4" />
-</Dialog> \ No newline at end of file
diff --git a/windows/dialogs/service-config.nsdinc b/windows/dialogs/service-config.nsdinc
deleted file mode 100644
index 58c350f2e..000000000
--- a/windows/dialogs/service-config.nsdinc
+++ /dev/null
@@ -1,56 +0,0 @@
-; =========================================================
-; This file was generated by NSISDialogDesigner 1.4.4.0
-; http://coolsoft.altervista.org/nsisdialogdesigner
-;
-; Do not edit it manually, use NSISDialogDesigner instead!
-; =========================================================
-
-; handle variables
-Var hCtl_service_config
-Var hCtl_service_config_StartServiceAfterInstall
-Var hCtl_service_config_LocalSystemAccountLabel
-Var hCtl_service_config_NetworkServiceAccountLabel
-Var hCtl_service_config_UseLocalSystemAccount
-Var hCtl_service_config_UseNetworkServiceAccount
-Var hCtl_service_config_Font1
-
-
-; dialog create function
-Function fnc_service_config_Create
-
- ; custom font definitions
- CreateFont $hCtl_service_config_Font1 "Microsoft Sans Serif" "8.25" "700"
-
- ; === service_config (type: Dialog) ===
- nsDialogs::Create 1018
- Pop $hCtl_service_config
- ${If} $hCtl_service_config == error
- Abort
- ${EndIf}
- !insertmacro MUI_HEADER_TEXT "Configure the service" "This controls what type of access the server gets to this system."
-
- ; === StartServiceAfterInstall (type: Checkbox) ===
- ${NSD_CreateCheckbox} 8u 118u 280u 15u "Start Service after Install"
- Pop $hCtl_service_config_StartServiceAfterInstall
- ${NSD_Check} $hCtl_service_config_StartServiceAfterInstall
-
- ; === LocalSystemAccountLabel (type: Label) ===
- ${NSD_CreateLabel} 8u 71u 280u 28u "The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary."
- Pop $hCtl_service_config_LocalSystemAccountLabel
-
- ; === NetworkServiceAccountLabel (type: Label) ===
- ${NSD_CreateLabel} 8u 24u 280u 28u "The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service."
- Pop $hCtl_service_config_NetworkServiceAccountLabel
-
- ; === UseLocalSystemAccount (type: RadioButton) ===
- ${NSD_CreateRadioButton} 8u 54u 280u 15u "Use Local System account"
- Pop $hCtl_service_config_UseLocalSystemAccount
- ${NSD_AddStyle} $hCtl_service_config_UseLocalSystemAccount ${WS_GROUP}
-
- ; === UseNetworkServiceAccount (type: RadioButton) ===
- ${NSD_CreateRadioButton} 8u 7u 280u 15u "Use Network Service account (Recommended)"
- Pop $hCtl_service_config_UseNetworkServiceAccount
- SendMessage $hCtl_service_config_UseNetworkServiceAccount ${WM_SETFONT} $hCtl_service_config_Font1 0
- ${NSD_Check} $hCtl_service_config_UseNetworkServiceAccount
-
-FunctionEnd
diff --git a/windows/dialogs/setuptype.nsddef b/windows/dialogs/setuptype.nsddef
deleted file mode 100644
index b55ceeaaa..000000000
--- a/windows/dialogs/setuptype.nsddef
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-This file was created by NSISDialogDesigner 1.4.4.0
-http://coolsoft.altervista.org/nsisdialogdesigner
-Do not edit manually!
--->
-<Dialog Name="setuptype" Title="Setup Type" Subtitle="Control how Jellyfin is installed.">
- <Label Name="InstallasaServiceLabel" Location="12, 115" Size="426, 46" Text="Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." TabIndex="0" />
- <RadioButton Name="InstallasaService" Location="12, 88" Size="426, 24" Text="Install as a Service (Advanced Users)" TabIndex="1" />
- <Label Name="BasicInstallLabel" Location="12, 39" Size="426, 46" Text="The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." TabIndex="2" />
- <RadioButton Name="BasicInstall" Location="12, 12" Size="426, 24" Text="Basic Install (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="3" />
-</Dialog> \ No newline at end of file
diff --git a/windows/dialogs/setuptype.nsdinc b/windows/dialogs/setuptype.nsdinc
deleted file mode 100644
index 8746ad2cc..000000000
--- a/windows/dialogs/setuptype.nsdinc
+++ /dev/null
@@ -1,50 +0,0 @@
-; =========================================================
-; This file was generated by NSISDialogDesigner 1.4.4.0
-; http://coolsoft.altervista.org/nsisdialogdesigner
-;
-; Do not edit it manually, use NSISDialogDesigner instead!
-; =========================================================
-
-; handle variables
-Var hCtl_setuptype
-Var hCtl_setuptype_InstallasaServiceLabel
-Var hCtl_setuptype_InstallasaService
-Var hCtl_setuptype_BasicInstallLabel
-Var hCtl_setuptype_BasicInstall
-Var hCtl_setuptype_Font1
-
-
-; dialog create function
-Function fnc_setuptype_Create
-
- ; custom font definitions
- CreateFont $hCtl_setuptype_Font1 "Microsoft Sans Serif" "8.25" "700"
-
- ; === setuptype (type: Dialog) ===
- nsDialogs::Create 1018
- Pop $hCtl_setuptype
- ${If} $hCtl_setuptype == error
- Abort
- ${EndIf}
- !insertmacro MUI_HEADER_TEXT "Setup Type" "Control how Jellyfin is installed."
-
- ; === InstallasaServiceLabel (type: Label) ===
- ${NSD_CreateLabel} 8u 71u 280u 28u "Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares."
- Pop $hCtl_setuptype_InstallasaServiceLabel
-
- ; === InstallasaService (type: RadioButton) ===
- ${NSD_CreateRadioButton} 8u 54u 280u 15u "Install as a Service (Advanced Users)"
- Pop $hCtl_setuptype_InstallasaService
- ${NSD_AddStyle} $hCtl_setuptype_InstallasaService ${WS_GROUP}
-
- ; === BasicInstallLabel (type: Label) ===
- ${NSD_CreateLabel} 8u 24u 280u 28u "The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4."
- Pop $hCtl_setuptype_BasicInstallLabel
-
- ; === BasicInstall (type: RadioButton) ===
- ${NSD_CreateRadioButton} 8u 7u 280u 15u "Basic Install (Recommended)"
- Pop $hCtl_setuptype_BasicInstall
- SendMessage $hCtl_setuptype_BasicInstall ${WM_SETFONT} $hCtl_setuptype_Font1 0
- ${NSD_Check} $hCtl_setuptype_BasicInstall
-
-FunctionEnd
diff --git a/windows/helpers/ShowError.nsh b/windows/helpers/ShowError.nsh
deleted file mode 100644
index 6e09b1e40..000000000
--- a/windows/helpers/ShowError.nsh
+++ /dev/null
@@ -1,10 +0,0 @@
-; Show error
-!macro ShowError TEXT RETRYLABEL
- MessageBox MB_ABORTRETRYIGNORE|MB_ICONSTOP "${TEXT}" IDIGNORE +2 IDRETRY ${RETRYLABEL}
- Abort
-!macroend
-
-!macro ShowErrorFinal TEXT
- MessageBox MB_OK|MB_ICONSTOP "${TEXT}"
- Abort
-!macroend
diff --git a/windows/helpers/StrSlash.nsh b/windows/helpers/StrSlash.nsh
deleted file mode 100644
index b8aa771aa..000000000
--- a/windows/helpers/StrSlash.nsh
+++ /dev/null
@@ -1,47 +0,0 @@
-; Adapted from: https://nsis.sourceforge.io/Another_String_Replace_(and_Slash/BackSlash_Converter) (2019-08-31)
-
-!macro _StrSlashConstructor out in
- Push "${in}"
- Push "\"
- Call StrSlash
- Pop ${out}
-!macroend
-
-!define StrSlash '!insertmacro "_StrSlashConstructor"'
-
-; Push $filenamestring (e.g. 'c:\this\and\that\filename.htm')
-; Push "\"
-; Call StrSlash
-; Pop $R0
-; ;Now $R0 contains 'c:/this/and/that/filename.htm'
-Function StrSlash
- Exch $R3 ; $R3 = needle ("\" or "/")
- Exch
- Exch $R1 ; $R1 = String to replacement in (haystack)
- Push $R2 ; Replaced haystack
- Push $R4 ; $R4 = not $R3 ("/" or "\")
- Push $R6
- Push $R7 ; Scratch reg
- StrCpy $R2 ""
- StrLen $R6 $R1
- StrCpy $R4 "\"
- StrCmp $R3 "/" loop
- StrCpy $R4 "/"
-loop:
- StrCpy $R7 $R1 1
- StrCpy $R1 $R1 $R6 1
- StrCmp $R7 $R3 found
- StrCpy $R2 "$R2$R7"
- StrCmp $R1 "" done loop
-found:
- StrCpy $R2 "$R2$R4"
- StrCmp $R1 "" done loop
-done:
- StrCpy $R3 $R2
- Pop $R7
- Pop $R6
- Pop $R4
- Pop $R2
- Pop $R1
- Exch $R3
-FunctionEnd
diff --git a/windows/jellyfin.nsi b/windows/jellyfin.nsi
deleted file mode 100644
index fada62d98..000000000
--- a/windows/jellyfin.nsi
+++ /dev/null
@@ -1,575 +0,0 @@
-!verbose 3
-SetCompressor /SOLID bzip2
-ShowInstDetails show
-ShowUninstDetails show
-Unicode True
-
-;--------------------------------
-!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh
-
- !include "MUI2.nsh"
- !include "Sections.nsh"
- !include "LogicLib.nsh"
-
- !include "helpers\ShowError.nsh"
-
-; Global variables that we'll use
- Var _JELLYFINVERSION_
- Var _JELLYFINDATADIR_
- Var _SETUPTYPE_
- Var _INSTALLSERVICE_
- Var _SERVICESTART_
- Var _SERVICEACCOUNTTYPE_
- Var _EXISTINGINSTALLATION_
- Var _EXISTINGSERVICE_
- Var _MAKESHORTCUTS_
- Var _FOLDEREXISTS_
-;
-!ifdef x64
- !define ARCH "x64"
- !define NAMESUFFIX "(64 bit)"
- !define INSTALL_DIRECTORY "$PROGRAMFILES64\Jellyfin\Server"
-!endif
-
-!ifdef x84
- !define ARCH "x86"
- !define NAMESUFFIX "(32 bit)"
- !define INSTALL_DIRECTORY "$PROGRAMFILES32\Jellyfin\Server"
-!endif
-
-!ifndef ARCH
- !error "Set the Arch with /Dx86 or /Dx64"
-!endif
-
-;--------------------------------
-
- !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\JellyfinServer" ;Registry to show up in Add/Remove Programs
- !define REG_CONFIG_KEY "Software\Jellyfin\Server" ;Registry to store all configuration
-
- !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ ;Align installer version with jellyfin.dll version
-
- Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} ${NAMESUFFIX}" ; This is referred in various header text labels
- OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows-${ARCH}.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe
- BrandingText "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} Installer" ; This shows in just over the buttons
-
-; installer attributes, these show up in details tab on installer properties
- VIProductVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIProductVersion format, should be X.X.X.X
- VIFileVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIFileVersion format, should be X.X.X.X
- VIAddVersionKey "ProductName" "Jellyfin Server"
- VIAddVersionKey "FileVersion" "${ver_1}.${ver_2}.${ver_3}.0"
- VIAddVersionKey "LegalCopyright" "(c) 2019 Jellyfin Contributors. Code released under the GNU General Public License"
- VIAddVersionKey "FileDescription" "Jellyfin Server: The Free Software Media System"
-
-;TODO, check defaults
- InstallDir ${INSTALL_DIRECTORY} ;Default installation folder
- InstallDirRegKey HKLM "${REG_CONFIG_KEY}" "InstallFolder" ;Read the registry for install folder,
-
- RequestExecutionLevel admin ; ask it upfront for service control, and installing in priv folders
-
- CRCCheck on ; make sure the installer wasn't corrupted while downloading
-
- !define MUI_ABORTWARNING ;Prompts user in case of aborting install
-
-; TODO: Replace with nice Jellyfin Icons
-!ifdef UXPATH
- !define MUI_ICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Installer Icon
- !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Uninstaller Icon
-
- !define MUI_HEADERIMAGE
- !define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp"
- !define MUI_WELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp"
- !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp"
-!endif
-
-;--------------------------------
-;Pages
-
-; Welcome Page
- !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server."
- !insertmacro MUI_PAGE_WELCOME
-; License Page
- !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL
-
-; Setup Type Page
- Page custom ShowSetupTypePage SetupTypePage_Config
-
-; Components Page
- !define MUI_PAGE_CUSTOMFUNCTION_PRE HideComponentsPage
- !insertmacro MUI_PAGE_COMPONENTS
- !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show
- !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog
- !insertmacro MUI_PAGE_DIRECTORY
-
-; Data folder Page
- !define MUI_PAGE_CUSTOMFUNCTION_PRE HideDataDirectoryPage ; Controls when to hide / show
- !define MUI_PAGE_HEADER_TEXT "Choose Data Location"
- !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server data."
- !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server data. To install in a different folder, click Browse and select another folder. Please make sure the folder exists and is accessible. Click Next to continue."
- !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data folder"
- !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_
- !insertmacro MUI_PAGE_DIRECTORY
-
-; Custom Dialogs
- !include "dialogs\setuptype.nsdinc"
- !include "dialogs\service-config.nsdinc"
- !include "dialogs\confirmation.nsdinc"
-
-; Select service account type
- #!define MUI_PAGE_CUSTOMFUNCTION_PRE HideServiceConfigPage ; Controls when to hide / show (This does not work for Page, might need to go PageEx)
- #!define MUI_PAGE_CUSTOMFUNCTION_SHOW fnc_service_config_Show
- #!define MUI_PAGE_CUSTOMFUNCTION_LEAVE ServiceConfigPage_Config
- #!insertmacro MUI_PAGE_CUSTOM ServiceAccountType
- Page custom ShowServiceConfigPage ServiceConfigPage_Config
-
-; Confirmation Page
- Page custom ShowConfirmationPage ; just letting the user know what they chose to install
-
-; Actual Installion Page
- !insertmacro MUI_PAGE_INSTFILES
-
- !insertmacro MUI_UNPAGE_CONFIRM
- !insertmacro MUI_UNPAGE_INSTFILES
- #!insertmacro MUI_UNPAGE_FINISH
-
-;--------------------------------
-;Languages; Add more languages later here if needed
- !insertmacro MUI_LANGUAGE "English"
-
-;--------------------------------
-;Installer Sections
-Section "!Jellyfin Server (required)" InstallJellyfinServer
- SectionIn RO ; Mandatory section, isn't this the whole purpose to run the installer.
-
- StrCmp "$_EXISTINGINSTALLATION_" "Yes" RunUninstaller CarryOn ; Silently uninstall in case of previous installation
-
- RunUninstaller:
- DetailPrint "Looking for uninstaller at $INSTDIR"
- FindFirst $0 $1 "$INSTDIR\Uninstall.exe"
- FindClose $0
- StrCmp $1 "" CarryOn ; the registry key was there but uninstaller was not found
-
- DetailPrint "Silently running the uninstaller at $INSTDIR"
- ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' $0
- DetailPrint "Uninstall finished, $0"
-
- CarryOn:
- ${If} $_EXISTINGSERVICE_ == 'Yes'
- ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0
- ${If} $0 <> 0
- MessageBox MB_OK|MB_ICONSTOP "Could not stop the Jellyfin Server service."
- Abort
- ${EndIf}
- DetailPrint "Stopped Jellyfin Server service, $0"
- ${EndIf}
-
- SetOutPath "$INSTDIR"
-
- File "/oname=icon.ico" "${UXPATH}\branding\NSIS\modern-install.ico"
- File /r $%InstallLocation%\*
-
-
-; Write the InstallFolder, DataFolder, Network Service info into the registry for later use
- WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "InstallFolder" "$INSTDIR"
- WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "DataFolder" "$_JELLYFINDATADIR_"
- WriteRegStr HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" "$_SERVICEACCOUNTTYPE_"
-
- !getdllversion "$%InstallLocation%\jellyfin.dll" ver_
- StrCpy $_JELLYFINVERSION_ "${ver_1}.${ver_2}.${ver_3}" ;
-
-; Write the uninstall keys for Windows
- WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin Server $_JELLYFINVERSION_ ${NAMESUFFIX}"
- WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
- WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0'
- WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project"
- WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.org/"
- WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_"
- WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1
- WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1
-
-;Create uninstaller
- WriteUninstaller "$INSTDIR\Uninstall.exe"
-SectionEnd
-
-Section "Jellyfin Server Service" InstallService
-${If} $_INSTALLSERVICE_ == "Yes" ; Only run this if we're going to install the service!
- ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
- DetailPrint "Jellyfin Server service statuscode, $0"
- ${If} $0 == 0
- InstallRetry:
- ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --service --datadir \"$_JELLYFINDATADIR_\"' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry
- ${EndIf}
- DetailPrint "Jellyfin Server Service install, $0"
- ${Else}
- DetailPrint "Jellyfin Server Service exists, updating..."
-
- ConfigureApplicationRetry:
- ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Application "$INSTDIR\jellyfin.exe"' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureApplicationRetry
- ${EndIf}
- DetailPrint "Jellyfin Server Service setting (Application), $0"
-
- ConfigureAppParametersRetry:
- ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --service --datadir \"$_JELLYFINDATADIR_\"' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureAppParametersRetry
- ${EndIf}
- DetailPrint "Jellyfin Server Service setting (AppParameters), $0"
- ${EndIf}
-
-
- Sleep 3000 ; Give time for Windows to catchup
- ConfigureStartRetry:
- ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Start SERVICE_DELAYED_AUTO_START' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureStartRetry
- ${EndIf}
- DetailPrint "Jellyfin Server Service setting (Start), $0"
-
- ConfigureDescriptionRetry:
- ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Description "Jellyfin Server: The Free Software Media System"' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDescriptionRetry
- ${EndIf}
- DetailPrint "Jellyfin Server Service setting (Description), $0"
- ConfigureDisplayNameRetry:
- ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer DisplayName "Jellyfin Server"' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDisplayNameRetry
-
- ${EndIf}
- DetailPrint "Jellyfin Server Service setting (DisplayName), $0"
-
- Sleep 3000
- ${If} $_SERVICEACCOUNTTYPE_ == "NetworkService" ; the default install using NSSM is Local System
- ConfigureNetworkServiceRetry:
- ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Objectname "Network Service"' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not configure the Jellyfin Server service account." ConfigureNetworkServiceRetry
- ${EndIf}
- DetailPrint "Jellyfin Server service account change, $0"
- ${EndIf}
-
- Sleep 3000
- ConfigureDefaultAppExit:
- ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppExit Default Exit' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not configure the Jellyfin Server service app exit action." ConfigureDefaultAppExit
- ${EndIf}
- DetailPrint "Jellyfin Server service exit action set, $0"
-${EndIf}
-
-SectionEnd
-
-Section "-start service" StartService
-${If} $_SERVICESTART_ == "Yes"
-${AndIf} $_INSTALLSERVICE_ == "Yes"
- StartRetry:
- ExecWait '"$INSTDIR\nssm.exe" start JellyfinServer' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not start the Jellyfin Server service." StartRetry
- ${EndIf}
- DetailPrint "Jellyfin Server service start, $0"
-${EndIf}
-SectionEnd
-
-Section "Create Shortcuts" CreateWinShortcuts
- ${If} $_MAKESHORTCUTS_ == "Yes"
- CreateDirectory "$SMPROGRAMS\Jellyfin Server"
- CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin (View Console).lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMAXIMIZED
- CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin Tray App.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0
- ;CreateShortCut "$DESKTOP\Jellyfin Server.lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMINIMIZED
- CreateShortCut "$DESKTOP\Jellyfin Server\Jellyfin Server.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0
- ${EndIf}
-SectionEnd
-
-;--------------------------------
-;Descriptions
-
-;Language strings
- LangString DESC_InstallJellyfinServer ${LANG_ENGLISH} "Install Jellyfin Server"
- LangString DESC_InstallService ${LANG_ENGLISH} "Install As a Service"
-
-;Assign language strings to sections
- !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
- !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfinServer} $(DESC_InstallJellyfinServer)
- !insertmacro MUI_DESCRIPTION_TEXT ${InstallService} $(DESC_InstallService)
- !insertmacro MUI_FUNCTION_DESCRIPTION_END
-
-;--------------------------------
-;Uninstaller Section
-
-Section "Uninstall"
-
- ReadRegStr $INSTDIR HKLM "${REG_CONFIG_KEY}" "InstallFolder" ; read the installation folder
- ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; read the data folder
- ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; read the account name
-
- DetailPrint "Jellyfin Install location: $INSTDIR"
- DetailPrint "Jellyfin Data folder: $_JELLYFINDATADIR_"
-
- MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain the Jellyfin Server data folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData
-
- RMDir /r /REBOOTOK "$_JELLYFINDATADIR_"
-
- PreserveData:
-
- ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
- DetailPrint "Jellyfin Server service statuscode, $0"
- IntCmp $0 0 NoServiceUninstall ; service doesn't exist, may be run from desktop shortcut
-
- Sleep 3000 ; Give time for Windows to catchup
-
- UninstallStopRetry:
- ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not stop the Jellyfin Server service." UninstallStopRetry
- ${EndIf}
- DetailPrint "Stopped Jellyfin Server service, $0"
-
- UninstallRemoveRetry:
- ExecWait '"$INSTDIR\nssm.exe" remove JellyfinServer confirm' $0
- ${If} $0 <> 0
- !insertmacro ShowError "Could not remove the Jellyfin Server service." UninstallRemoveRetry
- ${EndIf}
- DetailPrint "Removed Jellyfin Server service, $0"
-
- Sleep 3000 ; Give time for Windows to catchup
-
- NoServiceUninstall: ; existing install was present but no service was detected. Remove shortcuts if account is set to none
- ${If} $_SERVICEACCOUNTTYPE_ == "None"
- RMDir /r "$SMPROGRAMS\Jellyfin Server"
- Delete "$DESKTOP\Jellyfin Server.lnk"
- DetailPrint "Removed old shortcuts..."
- ${EndIf}
-
- Delete "$INSTDIR\*.*"
- RMDir /r /REBOOTOK "$INSTDIR\jellyfin-web"
- Delete "$INSTDIR\Uninstall.exe"
- RMDir /r /REBOOTOK "$INSTDIR"
-
- DeleteRegKey HKLM "Software\Jellyfin"
- DeleteRegKey HKLM "${REG_UNINST_KEY}"
-
-SectionEnd
-
-Function .onInit
-; Setting up defaults
- StrCpy $_INSTALLSERVICE_ "Yes"
- StrCpy $_SERVICESTART_ "Yes"
- StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService"
- StrCpy $_EXISTINGINSTALLATION_ "No"
- StrCpy $_EXISTINGSERVICE_ "No"
- StrCpy $_MAKESHORTCUTS_ "No"
-
- SetShellVarContext current
- StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server"
-
- System::Call 'kernel32::CreateMutex(p 0, i 0, t "JellyfinServerMutex") p .r1 ?e'
- Pop $R0
-
- StrCmp $R0 0 +3
- !insertmacro ShowErrorFinal "The installer is already running."
-
-;Detect if Jellyfin is already installed.
-; In case it is installed, let the user choose either
-; 1. Exit installer
-; 2. Upgrade without messing with data
-; 2a. Don't ask for any details, uninstall and install afresh with old settings
-
-; Read Registry for previous installation
- ClearErrors
- ReadRegStr "$0" HKLM "${REG_CONFIG_KEY}" "InstallFolder"
- IfErrors NoExisitingInstall
-
- DetailPrint "Existing Jellyfin Server detected at: $0"
- StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default
-
- StrCpy $_EXISTINGINSTALLATION_ "Yes" ; Set our flag to be used later
- SectionSetText ${InstallJellyfinServer} "Upgrade Jellyfin Server (required)" ; Change install text to "Upgrade"
-
- ; check if service was run using Network Service account
- ClearErrors
- ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default
-
- ClearErrors
- ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds
-
- ; Hide sections which will not be needed in case of previous install
- ; SectionSetText ${InstallService} ""
-
-; check if there is a service called Jellyfin, there should be
-; hack : nssm statuscode Jellyfin will return non zero return code in case it exists
- ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
- DetailPrint "Jellyfin Server service statuscode, $0"
- IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut
-
- ; if service was detected, set defaults going forward.
- StrCpy $_EXISTINGSERVICE_ "Yes"
- StrCpy $_INSTALLSERVICE_ "Yes"
- StrCpy $_SERVICESTART_ "Yes"
- StrCpy $_MAKESHORTCUTS_ "No"
- SectionSetText ${CreateWinShortcuts} ""
-
-
- NoService: ; existing install was present but no service was detected
- ${If} $_SERVICEACCOUNTTYPE_ == "None"
- StrCpy $_SETUPTYPE_ "Basic"
- StrCpy $_INSTALLSERVICE_ "No"
- StrCpy $_SERVICESTART_ "No"
- StrCpy $_MAKESHORTCUTS_ "Yes"
- ${EndIf}
-
-; Let the user know that we'll upgrade and provide an option to quit.
- MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin Server was detected, it'll be upgraded, settings will be retained. \
- $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade
- Quit ; Quit if the user is not sure about upgrade
-
- ProceedWithUpgrade:
-
- NoExisitingInstall: ; by this time, the variables have been correctly set to reflect previous install details
-
-FunctionEnd
-
-Function HideInstallDirectoryPage
- ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
- Abort
- ${EndIf}
-FunctionEnd
-
-Function HideDataDirectoryPage
- ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
- Abort
- ${EndIf}
-FunctionEnd
-
-Function HideServiceConfigPage
- ${If} $_INSTALLSERVICE_ == "No" ; Not running as a service, don't ask for service type
- ${OrIf} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
- Abort
- ${EndIf}
-FunctionEnd
-
-Function HideConfirmationPage
- ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
- Abort
- ${EndIf}
-FunctionEnd
-
-Function HideSetupTypePage
- ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for SetupType
- Abort
- ${EndIf}
-FunctionEnd
-
-Function HideComponentsPage
- ${If} $_SETUPTYPE_ == "Basic" ; Basic installation chosen, don't show components choice
- Abort
- ${EndIf}
-FunctionEnd
-
-; Setup Type dialog show function
-Function ShowSetupTypePage
- Call HideSetupTypePage
- Call fnc_setuptype_Create
- nsDialogs::Show
-FunctionEnd
-
-; Service Config dialog show function
-Function ShowServiceConfigPage
- Call HideServiceConfigPage
- Call fnc_service_config_Create
- nsDialogs::Show
-FunctionEnd
-
-; Confirmation dialog show function
-Function ShowConfirmationPage
- Call HideConfirmationPage
- Call fnc_confirmation_Create
- nsDialogs::Show
-FunctionEnd
-
-; Declare temp variables to read the options from the custom page.
-Var StartServiceAfterInstall
-Var UseNetworkServiceAccount
-Var UseLocalSystemAccount
-Var BasicInstall
-
-
-Function SetupTypePage_Config
-${NSD_GetState} $hCtl_setuptype_BasicInstall $BasicInstall
- IfFileExists "$LOCALAPPDATA\Jellyfin" folderfound foldernotfound ; if the folder exists, use this, otherwise, go with new default
- folderfound:
- StrCpy $_FOLDEREXISTS_ "Yes"
- Goto InstallCheck
- foldernotfound:
- StrCpy $_FOLDEREXISTS_ "No"
- Goto InstallCheck
-
-InstallCheck:
-${If} $BasicInstall == 1
- StrCpy $_SETUPTYPE_ "Basic"
- StrCpy $_INSTALLSERVICE_ "No"
- StrCpy $_SERVICESTART_ "No"
- StrCpy $_SERVICEACCOUNTTYPE_ "None"
- StrCpy $_MAKESHORTCUTS_ "Yes"
- ${If} $_FOLDEREXISTS_ == "Yes"
- StrCpy $_JELLYFINDATADIR_ "$LOCALAPPDATA\Jellyfin\"
- ${EndIf}
-${Else}
- StrCpy $_SETUPTYPE_ "Advanced"
- StrCpy $_INSTALLSERVICE_ "Yes"
- StrCpy $_MAKESHORTCUTS_ "No"
- ${If} $_FOLDEREXISTS_ == "Yes"
- MessageBox MB_OKCANCEL|MB_ICONINFORMATION "An existing data folder was detected.\
- $\r$\nBasic Setup is highly recommended.\
- $\r$\nIf you proceed, you will need to set up Jellyfin again." IDOK GoAhead IDCANCEL GoBack
- GoBack:
- Abort
- ${EndIf}
- GoAhead:
- StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server"
- SectionSetText ${CreateWinShortcuts} ""
-${EndIf}
-
-FunctionEnd
-
-Function ServiceConfigPage_Config
-${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall
-${If} $StartServiceAfterInstall == 1
- StrCpy $_SERVICESTART_ "Yes"
-${Else}
- StrCpy $_SERVICESTART_ "No"
-${EndIf}
-${NSD_GetState} $hCtl_service_config_UseNetworkServiceAccount $UseNetworkServiceAccount
-${NSD_GetState} $hCtl_service_config_UseLocalSystemAccount $UseLocalSystemAccount
-
-${If} $UseNetworkServiceAccount == 1
- StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService"
-${ElseIf} $UseLocalSystemAccount == 1
- StrCpy $_SERVICEACCOUNTTYPE_ "LocalSystem"
-${Else}
- !insertmacro ShowErrorFinal "Service account type not properly configured."
-${EndIf}
-
-FunctionEnd
-
-; This function handles the choices during component selection
-Function .onSelChange
-
-; If we are not installing service, we don't need to set the NetworkService account or StartService
- SectionGetFlags ${InstallService} $0
- ${If} $0 = ${SF_SELECTED}
- StrCpy $_INSTALLSERVICE_ "Yes"
- ${Else}
- StrCpy $_INSTALLSERVICE_ "No"
- StrCpy $_SERVICESTART_ "No"
- StrCpy $_SERVICEACCOUNTTYPE_ "None"
- ${EndIf}
-FunctionEnd
-
-Function .onInstSuccess
- #ExecShell "open" "http://localhost:8096"
-FunctionEnd
diff --git a/windows/legacy/install-jellyfin.ps1 b/windows/legacy/install-jellyfin.ps1
deleted file mode 100644
index e909a0468..000000000
--- a/windows/legacy/install-jellyfin.ps1
+++ /dev/null
@@ -1,460 +0,0 @@
-[CmdletBinding()]
-
-param(
- [Switch]$Quiet,
- [Switch]$InstallAsService,
- [System.Management.Automation.pscredential]$ServiceUser,
- [switch]$CreateDesktopShorcut,
- [switch]$LaunchJellyfin,
- [switch]$MigrateEmbyLibrary,
- [string]$InstallLocation,
- [string]$EmbyLibraryLocation,
- [string]$JellyfinLibraryLocation
-)
-<# This form was created using POSHGUI.com a free online gui designer for PowerShell
-.NAME
- Install-Jellyfin
-#>
-
-#This doesn't need to be used by default anymore, but I am keeping it in as a function for future use.
-function Elevate-Window {
- # Get the ID and security principal of the current user account
- $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
- $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
-
- # Get the security principal for the Administrator role
- $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
-
- # Check to see if we are currently running "as Administrator"
- if ($myWindowsPrincipal.IsInRole($adminRole))
- {
- # We are running "as Administrator" - so change the title and background color to indicate this
- $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)"
- $Host.UI.RawUI.BackgroundColor = "DarkBlue"
- clear-host
- }
- else
- {
- # We are not running "as Administrator" - so relaunch as administrator
-
- # Create a new process object that starts PowerShell
- $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell";
-
- # Specify the current script path and name as a parameter
- $newProcess.Arguments = $myInvocation.MyCommand.Definition;
-
- # Indicate that the process should be elevated
- $newProcess.Verb = "runas";
-
- # Start the new process
- [System.Diagnostics.Process]::Start($newProcess);
-
- # Exit from the current, unelevated, process
- exit
- }
-}
-
-#FIXME The install methods should be a function that takes all the params, the quiet flag should be a paramset
-
-if($Quiet.IsPresent -or $Quiet -eq $true){
- if([string]::IsNullOrEmpty($JellyfinLibraryLocation)){
- $Script:JellyfinDataDir = "$env:LOCALAPPDATA\jellyfin\"
- }else{
- $Script:JellyfinDataDir = $JellyfinLibraryLocation
- }
- if([string]::IsNullOrEmpty($InstallLocation)){
- $Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\"
- }else{
- $Script:DefaultJellyfinInstallDirectory = $InstallLocation
- }
-
- if([string]::IsNullOrEmpty($EmbyLibraryLocation)){
- $Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\data\"
- }else{
- $Script:defaultEmbyDataDir = $EmbyLibraryLocation
- }
-
- if($InstallAsService.IsPresent -or $InstallAsService -eq $true){
- $Script:InstallAsService = $true
- }else{$Script:InstallAsService = $false}
- if($null -eq $ServiceUser){
- $Script:InstallServiceAsUser = $false
- }else{
- $Script:InstallServiceAsUser = $true
- $Script:UserCredentials = $ServiceUser
- $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Local\jellyfin\"}
- if($CreateDesktopShorcut.IsPresent -or $CreateDesktopShorcut -eq $true) {$Script:CreateShortcut = $true}else{$Script:CreateShortcut = $false}
- if($MigrateEmbyLibrary.IsPresent -or $MigrateEmbyLibrary -eq $true){$Script:MigrateLibrary = $true}else{$Script:MigrateLibrary = $false}
- if($LaunchJellyfin.IsPresent -or $LaunchJellyfin -eq $true){$Script:StartJellyfin = $true}else{$Script:StartJellyfin = $false}
-
- if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){
- mkdir $Script:DefaultJellyfinInstallDirectory
- }
- Copy-Item -Path $PSScriptRoot/* -DestinationPath "$Script:DefaultJellyfinInstallDirectory/" -Force -Recurse
- if($Script:InstallAsService){
- if($Script:InstallServiceAsUser){
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
- Start-Sleep -Milliseconds 500
- &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)"
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
- }else{
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
- Start-Sleep -Milliseconds 500
- #&"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin ObjectName $Script:UserCredentials.UserName $Script:UserCredentials.GetNetworkCredential().Password
- #Set-Service -Name Jellyfin -Credential $Script:UserCredentials
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
- }
- }
- if($Script:MigrateLibrary){
- Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse
- Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse
- Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse
- Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse
- Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse
- }
- if($Script:CreateShortcut){
- $WshShell = New-Object -comObject WScript.Shell
- $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk")
- $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe"
- $Shortcut.Save()
- }
- if($Script:StartJellyfin){
- if($Script:InstallAsService){
- Get-Service Jellyfin | Start-Service
- }else{
- Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru
- }
- }
-}else{
-
-}
-Add-Type -AssemblyName System.Windows.Forms
-[System.Windows.Forms.Application]::EnableVisualStyles()
-
-$Script:JellyFinDataDir = "$env:LOCALAPPDATA\jellyfin\"
-$Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\"
-$Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\"
-$Script:InstallAsService = $False
-$Script:InstallServiceAsUser = $false
-$Script:CreateShortcut = $false
-$Script:MigrateLibrary = $false
-$Script:StartJellyfin = $false
-
-function InstallJellyfin {
- Write-Host "Install as service: $Script:InstallAsService"
- Write-Host "Install as serviceuser: $Script:InstallServiceAsUser"
- Write-Host "Create Shortcut: $Script:CreateShortcut"
- Write-Host "MigrateLibrary: $Script:MigrateLibrary"
- $GUIElementsCollection | ForEach-Object {
- $_.Enabled = $false
- }
- Write-Host "Making Jellyfin directory"
- $ProgressBar.Minimum = 1
- $ProgressBar.Maximum = 100
- $ProgressBar.Value = 1
- if($Script:DefaultJellyfinInstallDirectory -ne $InstallLocationBox.Text){
- Write-Host "Custom Install Location Chosen: $($InstallLocationBox.Text)"
- $Script:DefaultJellyfinInstallDirectory = $InstallLocationBox.Text
- }
- if($Script:JellyfinDataDir -ne $CustomLibraryBox.Text){
- Write-Host "Custom Library Location Chosen: $($CustomLibraryBox.Text)"
- $Script:JellyfinDataDir = $CustomLibraryBox.Text
- }
- if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){
- mkdir $Script:DefaultJellyfinInstallDirectory
- }
- Write-Host "Copying Jellyfin Data"
- $progressbar.Value = 10
- Copy-Item -Path $PSScriptRoot/* -Destination $Script:DefaultJellyfinInstallDirectory/ -Force -Recurse
- Write-Host "Finished Copying"
- $ProgressBar.Value = 50
- if($Script:InstallAsService){
- if($Script:InstallServiceAsUser){
- Write-Host "Installing Service as user $($Script:UserCredentials.UserName)"
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
- Start-Sleep -Milliseconds 2000
- &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)"
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
- }else{
- Write-Host "Installing Service as LocalSystem"
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
- Start-Sleep -Milliseconds 2000
- &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
- }
- }
- $progressbar.Value = 60
- if($Script:MigrateLibrary){
- if($Script:defaultEmbyDataDir -ne $LibraryLocationBox.Text){
- Write-Host "Custom location defined for emby library: $($LibraryLocationBox.Text)"
- $Script:defaultEmbyDataDir = $LibraryLocationBox.Text
- }
- Write-Host "Copying emby library from $Script:defaultEmbyDataDir to $Script:JellyFinDataDir"
- Write-Host "This could take a while depending on the size of your library. Please be patient"
- Write-Host "Copying config"
- Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse
- Write-Host "Copying cache"
- Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse
- Write-Host "Copying data"
- Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse
- Write-Host "Copying metadata"
- Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse
- Write-Host "Copying root dir"
- Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse
- }
- $progressbar.Value = 80
- if($Script:CreateShortcut){
- Write-Host "Creating Shortcut"
- $WshShell = New-Object -comObject WScript.Shell
- $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk")
- $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe"
- $Shortcut.Save()
- }
- $ProgressBar.Value = 90
- if($Script:StartJellyfin){
- if($Script:InstallAsService){
- Write-Host "Starting Jellyfin Service"
- Get-Service Jellyfin | Start-Service
- }else{
- Write-Host "Starting Jellyfin"
- Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru
- }
- }
- $progressbar.Value = 100
- Write-Host Finished
- $wshell = New-Object -ComObject Wscript.Shell
- $wshell.Popup("Operation Completed",0,"Done",0x1)
- $InstallForm.Close()
-}
-function ServiceBoxCheckChanged {
- if($InstallAsServiceCheck.Checked){
- $Script:InstallAsService = $true
- $ServiceUserLabel.Visible = $true
- $ServiceUserLabel.Enabled = $true
- $ServiceUserBox.Visible = $true
- $ServiceUserBox.Enabled = $true
- }else{
- $Script:InstallAsService = $false
- $ServiceUserLabel.Visible = $false
- $ServiceUserLabel.Enabled = $false
- $ServiceUserBox.Visible = $false
- $ServiceUserBox.Enabled = $false
- }
-}
-function UserSelect {
- if($ServiceUserBox.Text -eq 'Local System')
- {
- $Script:InstallServiceAsUser = $false
- $Script:UserCredentials = $null
- $ServiceUserBox.Items.RemoveAt(1)
- $ServiceUserBox.Items.Add("Custom User")
- }elseif($ServiceUserBox.Text -eq 'Custom User'){
- $Script:InstallServiceAsUser = $true
- $Script:UserCredentials = Get-Credential -Message "Please enter the credentials of the user you with to run Jellyfin Service as" -UserName $env:USERNAME
- $ServiceUserBox.Items[1] = "$($Script:UserCredentials.UserName)"
- }
-}
-function CreateShortcutBoxCheckChanged {
- if($CreateShortcutCheck.Checked){
- $Script:CreateShortcut = $true
- }else{
- $Script:CreateShortcut = $False
- }
-}
-function StartJellyFinBoxCheckChanged {
- if($StartProgramCheck.Checked){
- $Script:StartJellyfin = $true
- }else{
- $Script:StartJellyfin = $false
- }
-}
-
-function CustomLibraryCheckChanged {
- if($CustomLibraryCheck.Checked){
- $Script:UseCustomLibrary = $true
- $CustomLibraryBox.Enabled = $true
- }else{
- $Script:UseCustomLibrary = $false
- $CustomLibraryBox.Enabled = $false
- }
-}
-
-function MigrateLibraryCheckboxChanged {
-
- if($MigrateLibraryCheck.Checked){
- $Script:MigrateLibrary = $true
- $LibraryMigrationLabel.Visible = $true
- $LibraryMigrationLabel.Enabled = $true
- $LibraryLocationBox.Visible = $true
- $LibraryLocationBox.Enabled = $true
- }else{
- $Script:MigrateLibrary = $false
- $LibraryMigrationLabel.Visible = $false
- $LibraryMigrationLabel.Enabled = $false
- $LibraryLocationBox.Visible = $false
- $LibraryLocationBox.Enabled = $false
- }
-
-}
-
-
-#region begin GUI{
-
-$InstallForm = New-Object system.Windows.Forms.Form
-$InstallForm.ClientSize = '320,240'
-$InstallForm.text = "Terrible Jellyfin Installer"
-$InstallForm.TopMost = $false
-
-$GUIElementsCollection = @()
-
-$InstallButton = New-Object system.Windows.Forms.Button
-$InstallButton.text = "Install"
-$InstallButton.width = 60
-$InstallButton.height = 30
-$InstallButton.location = New-Object System.Drawing.Point(5,5)
-$InstallButton.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $InstallButton
-
-$ProgressBar = New-Object system.Windows.Forms.ProgressBar
-$ProgressBar.width = 245
-$ProgressBar.height = 30
-$ProgressBar.location = New-Object System.Drawing.Point(70,5)
-
-$InstallLocationLabel = New-Object system.Windows.Forms.Label
-$InstallLocationLabel.text = "Install Location"
-$InstallLocationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
-$InstallLocationLabel.AutoSize = $true
-$InstallLocationLabel.width = 100
-$InstallLocationLabel.height = 20
-$InstallLocationLabel.location = New-Object System.Drawing.Point(5,50)
-$InstallLocationLabel.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $InstallLocationLabel
-
-$InstallLocationBox = New-Object system.Windows.Forms.TextBox
-$InstallLocationBox.multiline = $false
-$InstallLocationBox.width = 205
-$InstallLocationBox.height = 20
-$InstallLocationBox.location = New-Object System.Drawing.Point(110,50)
-$InstallLocationBox.Text = $Script:DefaultJellyfinInstallDirectory
-$InstallLocationBox.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $InstallLocationBox
-
-$CustomLibraryCheck = New-Object system.Windows.Forms.CheckBox
-$CustomLibraryCheck.text = "Custom Library Location:"
-$CustomLibraryCheck.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
-$CustomLibraryCheck.AutoSize = $false
-$CustomLibraryCheck.width = 180
-$CustomLibraryCheck.height = 20
-$CustomLibraryCheck.location = New-Object System.Drawing.Point(5,75)
-$CustomLibraryCheck.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $CustomLibraryCheck
-
-$CustomLibraryBox = New-Object system.Windows.Forms.TextBox
-$CustomLibraryBox.multiline = $false
-$CustomLibraryBox.width = 130
-$CustomLibraryBox.height = 20
-$CustomLibraryBox.location = New-Object System.Drawing.Point(185,75)
-$CustomLibraryBox.Text = $Script:JellyFinDataDir
-$CustomLibraryBox.Font = 'Microsoft Sans Serif,10'
-$CustomLibraryBox.Enabled = $false
-$GUIElementsCollection += $CustomLibraryBox
-
-$InstallAsServiceCheck = New-Object system.Windows.Forms.CheckBox
-$InstallAsServiceCheck.text = "Install as Service"
-$InstallAsServiceCheck.AutoSize = $false
-$InstallAsServiceCheck.width = 140
-$InstallAsServiceCheck.height = 20
-$InstallAsServiceCheck.location = New-Object System.Drawing.Point(5,125)
-$InstallAsServiceCheck.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $InstallAsServiceCheck
-
-$ServiceUserLabel = New-Object system.Windows.Forms.Label
-$ServiceUserLabel.text = "Run Service As:"
-$ServiceUserLabel.AutoSize = $true
-$ServiceUserLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
-$ServiceUserLabel.width = 100
-$ServiceUserLabel.height = 20
-$ServiceUserLabel.location = New-Object System.Drawing.Point(15,145)
-$ServiceUserLabel.Font = 'Microsoft Sans Serif,10'
-$ServiceUserLabel.Visible = $false
-$ServiceUserLabel.Enabled = $false
-$GUIElementsCollection += $ServiceUserLabel
-
-$ServiceUserBox = New-Object system.Windows.Forms.ComboBox
-$ServiceUserBox.text = "Run Service As"
-$ServiceUserBox.width = 195
-$ServiceUserBox.height = 20
-@('Local System','Custom User') | ForEach-Object {[void] $ServiceUserBox.Items.Add($_)}
-$ServiceUserBox.location = New-Object System.Drawing.Point(120,145)
-$ServiceUserBox.Font = 'Microsoft Sans Serif,10'
-$ServiceUserBox.Visible = $false
-$ServiceUserBox.Enabled = $false
-$ServiceUserBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
-$GUIElementsCollection += $ServiceUserBox
-
-$MigrateLibraryCheck = New-Object system.Windows.Forms.CheckBox
-$MigrateLibraryCheck.text = "Import Emby/Old JF Library"
-$MigrateLibraryCheck.AutoSize = $false
-$MigrateLibraryCheck.width = 160
-$MigrateLibraryCheck.height = 20
-$MigrateLibraryCheck.location = New-Object System.Drawing.Point(5,170)
-$MigrateLibraryCheck.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $MigrateLibraryCheck
-
-$LibraryMigrationLabel = New-Object system.Windows.Forms.Label
-$LibraryMigrationLabel.text = "Emby/Old JF Library Path"
-$LibraryMigrationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
-$LibraryMigrationLabel.AutoSize = $false
-$LibraryMigrationLabel.width = 120
-$LibraryMigrationLabel.height = 20
-$LibraryMigrationLabel.location = New-Object System.Drawing.Point(15,190)
-$LibraryMigrationLabel.Font = 'Microsoft Sans Serif,10'
-$LibraryMigrationLabel.Visible = $false
-$LibraryMigrationLabel.Enabled = $false
-$GUIElementsCollection += $LibraryMigrationLabel
-
-$LibraryLocationBox = New-Object system.Windows.Forms.TextBox
-$LibraryLocationBox.multiline = $false
-$LibraryLocationBox.width = 175
-$LibraryLocationBox.height = 20
-$LibraryLocationBox.location = New-Object System.Drawing.Point(140,190)
-$LibraryLocationBox.Text = $Script:defaultEmbyDataDir
-$LibraryLocationBox.Font = 'Microsoft Sans Serif,10'
-$LibraryLocationBox.Visible = $false
-$LibraryLocationBox.Enabled = $false
-$GUIElementsCollection += $LibraryLocationBox
-
-$CreateShortcutCheck = New-Object system.Windows.Forms.CheckBox
-$CreateShortcutCheck.text = "Desktop Shortcut"
-$CreateShortcutCheck.AutoSize = $false
-$CreateShortcutCheck.width = 150
-$CreateShortcutCheck.height = 20
-$CreateShortcutCheck.location = New-Object System.Drawing.Point(5,215)
-$CreateShortcutCheck.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $CreateShortcutCheck
-
-$StartProgramCheck = New-Object system.Windows.Forms.CheckBox
-$StartProgramCheck.text = "Start Jellyfin"
-$StartProgramCheck.AutoSize = $false
-$StartProgramCheck.width = 160
-$StartProgramCheck.height = 20
-$StartProgramCheck.location = New-Object System.Drawing.Point(160,215)
-$StartProgramCheck.Font = 'Microsoft Sans Serif,10'
-$GUIElementsCollection += $StartProgramCheck
-
-$InstallForm.controls.AddRange($GUIElementsCollection)
-$InstallForm.Controls.Add($ProgressBar)
-
-#region gui events {
-$InstallButton.Add_Click({ InstallJellyfin })
-$CustomLibraryCheck.Add_CheckedChanged({CustomLibraryCheckChanged})
-$InstallAsServiceCheck.Add_CheckedChanged({ServiceBoxCheckChanged})
-$ServiceUserBox.Add_SelectedValueChanged({ UserSelect })
-$MigrateLibraryCheck.Add_CheckedChanged({MigrateLibraryCheckboxChanged})
-$CreateShortcutCheck.Add_CheckedChanged({CreateShortcutBoxCheckChanged})
-$StartProgramCheck.Add_CheckedChanged({StartJellyFinBoxCheckChanged})
-#endregion events }
-
-#endregion GUI }
-
-
-[void]$InstallForm.ShowDialog()
diff --git a/windows/legacy/install.bat b/windows/legacy/install.bat
deleted file mode 100644
index e21479a79..000000000
--- a/windows/legacy/install.bat
+++ /dev/null
@@ -1 +0,0 @@
-powershell.exe -executionpolicy Bypass -file install-jellyfin.ps1